diff --git a/.github/stale.yml b/.github/stale.yml index 10589b97ea9b3..0b9283fde06c7 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale - daysUntilStale: 76 +daysUntilStale: 76 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. @@ -18,7 +18,7 @@ exemptLabels: - "Progress: dev in progress" - "Progress: PR in progress" - "Progress: done" - - "B2B: GraphQL" + - "B2B: GraphQL" - "Progress: PR Created" - "PAP" - "Project: Login as Customer" diff --git a/app/code/Magento/AdminNotification/Block/ToolbarEntry.php b/app/code/Magento/AdminNotification/Block/ToolbarEntry.php index c097edfd8af65..42ca68177cb83 100644 --- a/app/code/Magento/AdminNotification/Block/ToolbarEntry.php +++ b/app/code/Magento/AdminNotification/Block/ToolbarEntry.php @@ -10,7 +10,6 @@ * Toolbar entry that shows latest notifications * * @api - * @author Magento Core Team * @since 100.0.2 */ class ToolbarEntry extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/AdminNotification/Model/Feed.php b/app/code/Magento/AdminNotification/Model/Feed.php index b99a8bbbc9031..ac1e631cc3f33 100644 --- a/app/code/Magento/AdminNotification/Model/Feed.php +++ b/app/code/Magento/AdminNotification/Model/Feed.php @@ -12,7 +12,6 @@ /** * AdminNotification Feed model * - * @author Magento Core Team * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 diff --git a/app/code/Magento/AdminNotification/Model/InboxInterface.php b/app/code/Magento/AdminNotification/Model/InboxInterface.php index 4e87822763fc3..5e61c3dd680c9 100644 --- a/app/code/Magento/AdminNotification/Model/InboxInterface.php +++ b/app/code/Magento/AdminNotification/Model/InboxInterface.php @@ -8,7 +8,6 @@ /** * AdminNotification Inbox interface * - * @author Magento Core Team * @api * @since 100.0.2 */ diff --git a/app/code/Magento/AdminNotification/Model/NotificationService.php b/app/code/Magento/AdminNotification/Model/NotificationService.php index d44e98aaf2203..a13efe2136a6f 100644 --- a/app/code/Magento/AdminNotification/Model/NotificationService.php +++ b/app/code/Magento/AdminNotification/Model/NotificationService.php @@ -8,7 +8,6 @@ /** * Notification service model * - * @author Magento Core Team * @api * @since 100.0.2 */ diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php index e12419155d52b..1a59d15e40c7a 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php @@ -6,8 +6,6 @@ /** * AdminNotification Inbox model - * - * @author Magento Core Team */ namespace Magento\AdminNotification\Model\ResourceModel\Grid; diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php index 44ec765b9d0a2..bf4f91cc6ae80 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php @@ -9,8 +9,6 @@ * AdminNotification Inbox model * * @api - * @author Magento Core Team - * @api * @since 100.0.2 */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php index b9e77f8a35295..9504c2f2d10f7 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php @@ -6,8 +6,6 @@ /** * Collection of unread notifications - * - * @author Magento Core Team */ namespace Magento\AdminNotification\Model\ResourceModel\Inbox\Collection; diff --git a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php index 24ef712c0f61f..a244ad1fb9a0f 100644 --- a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php +++ b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php @@ -9,8 +9,7 @@ /** * AdminNotification observer - * - * @author Magento Core Team + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class PredispatchAdminActionControllerObserver implements ObserverInterface { diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php index 403a4d12cc17b..401e9d666103e 100644 --- a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php @@ -9,7 +9,6 @@ * Search queries relations grid container * * @api - * @author Magento Core Team * @since 100.0.2 */ class Edit extends \Magento\Backend\Block\Widget\Grid\Container diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php index 6bdfd3b0dd143..add3e244be851 100644 --- a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php @@ -9,7 +9,6 @@ * Search query relations edit grid * * @api - * @author Magento Core Team * @since 100.0.2 */ class Grid extends \Magento\Backend\Block\Widget\Grid diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php index c19c1d67d81f7..9be5d0c201841 100644 --- a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php @@ -8,7 +8,6 @@ /** * Catalog search recommendations resource model * - * @author Magento Core Team * @api * @since 100.0.2 */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php index 6ab039aa27849..efb7d6dbbeff3 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php @@ -42,6 +42,8 @@ public function __construct( } /** + * Construct block + * * @return void */ protected function _construct() @@ -51,6 +53,14 @@ protected function _construct() parent::_construct(); + $this->buttonList->update('save', 'label', __('Save Attribute')); + $this->buttonList->update('save', 'class', 'save primary'); + $this->buttonList->update( + 'save', + 'data_attribute', + ['mage-init' => ['button' => ['event' => 'save', 'target' => '#edit_form']]] + ); + if ($this->getRequest()->getParam('popup')) { $this->buttonList->remove('back'); if ($this->getRequest()->getParam('product_tab') != 'variations') { @@ -64,6 +74,8 @@ protected function _construct() 100 ); } + $this->buttonList->update('reset', 'level', 10); + $this->buttonList->update('save', 'class', 'save action-secondary'); } else { $this->addButton( 'save_and_edit_button', @@ -79,14 +91,6 @@ protected function _construct() ); } - $this->buttonList->update('save', 'label', __('Save Attribute')); - $this->buttonList->update('save', 'class', 'save primary'); - $this->buttonList->update( - 'save', - 'data_attribute', - ['mage-init' => ['button' => ['event' => 'save', 'target' => '#edit_form']]] - ); - $entityAttribute = $this->_coreRegistry->registry('entity_attribute'); if (!$entityAttribute || !$entityAttribute->getIsUserDefined()) { $this->buttonList->remove('delete'); @@ -96,14 +100,14 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function addButton($buttonId, $data, $level = 0, $sortOrder = 0, $region = 'toolbar') { if ($this->getRequest()->getParam('popup')) { $region = 'header'; } - parent::addButton($buttonId, $data, $level, $sortOrder, $region); + return parent::addButton($buttonId, $data, $level, $sortOrder, $region); } /** diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 6cec9bf3ef88a..b181a5392905b 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -367,7 +367,7 @@ public function getIdentities() $identities[] = $item->getIdentities(); } } - $identities = array_merge(...$identities); + $identities = array_merge([], ...$identities); return $identities; } diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php index 387fac770c5bc..42f610f89768d 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php @@ -143,11 +143,11 @@ public function getItems() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getItems() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } /** diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php index ac66392efe5dc..adcb1b5666560 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php @@ -267,10 +267,10 @@ public function getItemLimit($type = '') */ public function getIdentities() { - $identities = array_map(function (DataObject $item) { - return $item->getIdentities(); - }, $this->getItems()) ?: [[]]; - - return array_merge(...$identities); + $identities = []; + foreach ($this->getItems() as $item) { + $identities[] = $item->getIdentities(); + } + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php b/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php new file mode 100644 index 0000000000000..a02d589fae055 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php @@ -0,0 +1,96 @@ +wysiwygValidator = $wysiwygValidator; + } + + /** + * Validate user HTML value. + * + * @param DataObject $object + * @return void + * @throws LocalizedException + */ + private function validateHtml(DataObject $object): void + { + $attribute = $this->getAttribute(); + $code = $attribute->getAttributeCode(); + if ($attribute instanceof Attribute && $attribute->getIsHtmlAllowedOnFront()) { + $value = $object->getData($code); + if ($value + && is_string($value) + && (!($object instanceof AbstractModel) || $object->getData($code) !== $object->getOrigData($code)) + ) { + try { + $this->wysiwygValidator->validate($object->getData($code)); + } catch (ValidationException $exception) { + $attributeException = new Exception( + __( + 'Using restricted HTML elements for "%1". %2', + $attribute->getName(), + $exception->getMessage() + ), + $exception + ); + $attributeException->setAttributeCode($code)->setPart('backend'); + throw $attributeException; + } + } + } + } + + /** + * @inheritDoc + */ + public function beforeSave($object) + { + parent::beforeSave($object); + $this->validateHtml($object); + + return $this; + } + + /** + * @inheritDoc + */ + public function validate($object) + { + $isValid = parent::validate($object); + if ($isValid) { + $this->validateHtml($object); + } + + return $isValid; + } +} diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 0ce52b966c32c..7082fa4747fdc 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -7,10 +7,16 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Model\CategoryRepository\PopulateWithValues; +use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Repository for categories. @@ -25,27 +31,27 @@ class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInter protected $instances = []; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Catalog\Model\CategoryFactory + * @var CategoryFactory */ protected $categoryFactory; /** - * @var \Magento\Catalog\Model\ResourceModel\Category + * @var CategoryResource */ protected $categoryResource; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ private $extensibleDataObjectConverter; @@ -57,28 +63,37 @@ class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInter protected $useConfigFields = ['available_sort_by', 'default_sort_by', 'filter_price_range']; /** - * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory - * @param \Magento\Catalog\Model\ResourceModel\Category $categoryResource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @var PopulateWithValues + */ + private $populateWithValues; + + /** + * @param CategoryFactory $categoryFactory + * @param CategoryResource $categoryResource + * @param StoreManagerInterface $storeManager + * @param PopulateWithValues|null $populateWithValues */ public function __construct( - \Magento\Catalog\Model\CategoryFactory $categoryFactory, - \Magento\Catalog\Model\ResourceModel\Category $categoryResource, - \Magento\Store\Model\StoreManagerInterface $storeManager + CategoryFactory $categoryFactory, + CategoryResource $categoryResource, + StoreManagerInterface $storeManager, + ?PopulateWithValues $populateWithValues ) { $this->categoryFactory = $categoryFactory; $this->categoryResource = $categoryResource; $this->storeManager = $storeManager; + $objectManager = ObjectManager::getInstance(); + $this->populateWithValues = $populateWithValues ?? $objectManager->get(PopulateWithValues::class); } /** * @inheritdoc */ - public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) + public function save(CategoryInterface $category) { $storeId = (int)$this->storeManager->getStore()->getId(); $existingData = $this->getExtensibleDataObjectConverter() - ->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class); + ->toNestedArray($category, [], CategoryInterface::class); $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); $existingData['store_id'] = $storeId; @@ -110,7 +125,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) $existingData['parent_id'] = $parentId; $existingData['level'] = null; } - $category->addData($existingData); + $this->populateWithValues->execute($category, $existingData); try { $this->validateCategory($category); $this->categoryResource->save($category); @@ -151,7 +166,7 @@ public function get($categoryId, $storeId = null) /** * @inheritdoc */ - public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category) + public function delete(CategoryInterface $category) { try { $categoryId = $category->getId(); @@ -213,15 +228,15 @@ protected function validateCategory(Category $category) /** * Lazy loader for the converter. * - * @return \Magento\Framework\Api\ExtensibleDataObjectConverter + * @return ExtensibleDataObjectConverter * * @deprecated 101.0.0 */ private function getExtensibleDataObjectConverter() { if ($this->extensibleDataObjectConverter === null) { - $this->extensibleDataObjectConverter = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\ExtensibleDataObjectConverter::class); + $this->extensibleDataObjectConverter = ObjectManager::getInstance() + ->get(ExtensibleDataObjectConverter::class); } return $this->extensibleDataObjectConverter; } @@ -229,13 +244,13 @@ private function getExtensibleDataObjectConverter() /** * Lazy loader for the metadata pool. * - * @return \Magento\Framework\EntityManager\MetadataPool + * @return MetadataPool */ private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->metadataPool = ObjectManager::getInstance() + ->get(MetadataPool::class); } return $this->metadataPool; } diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php new file mode 100644 index 0000000000000..c6feb049e1a10 --- /dev/null +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -0,0 +1,153 @@ +scopeOverriddenValue = $scopeOverriddenValue; + $this->attributeRepository = $attributeRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + } + + /** + * Set null to entity default values + * + * @param CategoryInterface $category + * @param array $existingData + * @return void + */ + public function execute(CategoryInterface $category, array $existingData): void + { + $storeId = $existingData['store_id'] ?? Store::DEFAULT_STORE_ID; + if ((int)$storeId !== Store::DEFAULT_STORE_ID) { + $overriddenValues = array_filter( + $category->getData(), + function ($key) use ($category, $storeId) { + /** @var Category $category */ + return $this->scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + $key, + $storeId + ); + }, + ARRAY_FILTER_USE_KEY + ); + $defaultValues = array_diff_key($category->getData(), $overriddenValues); + array_walk( + $defaultValues, + function (&$value, $key) { + $attributes = $this->getAttributes(); + if (isset($attributes[$key]) && !$attributes[$key]->isStatic()) { + $value = null; + } + } + ); + $category->addData($defaultValues); + } + + $category->addData($existingData); + $useDefaultAttributes = array_filter( + $category->getData(), + function ($attributeValue) { + return null === $attributeValue; + } + ); + $category->setData( + 'use_default', + array_map( + function () { + return true; + }, + $useDefaultAttributes + ) + ); + } + + /** + * Returns entity attributes. + * + * @return AttributeInterface[] + */ + private function getAttributes(): array + { + if ($this->attributes) { + return $this->attributes; + } + + $searchResult = $this->attributeRepository->getList( + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('is_global') + ->setConditionType('in') + ->setValue([ScopedAttributeInterface::SCOPE_STORE, ScopedAttributeInterface::SCOPE_WEBSITE]) + ->create() + ] + )->create() + ); + + $this->attributes = []; + foreach ($searchResult->getItems() as $attribute) { + $this->attributes[$attribute->getAttributeCode()] = $attribute; + } + + return $this->attributes; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php index 936e6163cbcc5..0c0c72b0322dc 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php @@ -55,7 +55,10 @@ public function afterGetTableName( string $result, $modelEntity ) { - if (!is_array($modelEntity) && $modelEntity === AbstractAction::MAIN_INDEX_TABLE) { + if (!is_array($modelEntity) && + $modelEntity === AbstractAction::MAIN_INDEX_TABLE && + $this->storeManager->getStore()->getId() + ) { $catalogCategoryProductDimension = new Dimension( \Magento\Store\Model\Store::ENTITY, $this->storeManager->getStore()->getId() diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index edd68422ec4ac..861f7c9c1c50e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -270,14 +270,14 @@ private function getCategoryIdsFromIndex(array $productIds): array ); $categoryIds[] = $storeCategories; } - $categoryIds = array_merge(...$categoryIds); + $categoryIds = array_merge([], ...$categoryIds); $parentCategories = [$categoryIds]; foreach ($categoryIds as $categoryId) { $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); $parentCategories[] = $parentIds; } - $categoryIds = array_unique(array_merge(...$parentCategories)); + $categoryIds = array_unique(array_merge([], ...$parentCategories)); return $categoryIds; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php index 99d75186eca8c..a0af09edf14c5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -261,7 +261,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $select->from( ['et' => $entityTemporaryTableName], - array_merge(...$allColumns) + array_merge([], ...$allColumns) )->joinInner( ['e' => $this->resource->getTableName('catalog_product_entity')], 'e.entity_id = et.entity_id', @@ -306,7 +306,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $allColumns[] = $columnValueNames; } } - $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge(...$allColumns), false); + $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge([], ...$allColumns), false); $this->_connection->query($sql); } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 1f3ecf8f967b5..82d252acd9909 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -836,10 +836,7 @@ public function getStoreIds() $storeIds[] = $websiteStores; } } - if ($storeIds) { - $storeIds = array_merge(...$storeIds); - } - $this->setStoreIds($storeIds); + $this->setStoreIds(array_merge([], ...$storeIds)); } return $this->getData('store_ids'); } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 36ef1826462b0..7d458401c950e 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -12,9 +12,6 @@ use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; use Magento\Catalog\Model\ProductIdLocatorInterface; -/** - * Tier price storage. - */ class TierPriceStorage implements TierPriceStorageInterface { /** @@ -220,7 +217,7 @@ private function retrieveAffectedIds(array $skus): array $affectedIds[] = array_keys($productId); } - return $affectedIds ? array_unique(array_merge(...$affectedIds)) : []; + return array_unique(array_merge([], ...$affectedIds)); } /** diff --git a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php index 4bc400605a429..1d5ef722db8b1 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php +++ b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php @@ -103,7 +103,7 @@ private function extractRequestedLinkTypes(array $criteria): array if (count($linkTypesToLoad) === 1) { $linkTypesToLoad = $linkTypesToLoad[0]; } else { - $linkTypesToLoad = array_merge(...$linkTypesToLoad); + $linkTypesToLoad = array_merge([], ...$linkTypesToLoad); } $linkTypesToLoad = array_flip($linkTypesToLoad); $linkTypes = array_filter( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index d1769ded93d29..07ce84c7cd62e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -6,7 +6,9 @@ namespace Magento\Catalog\Model\ResourceModel\Eav; +use Magento\Catalog\Model\Attribute\Backend\DefaultBackend; use Magento\Catalog\Model\Attribute\LockValidatorInterface; +use Magento\Eav\Model\Entity; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; @@ -902,4 +904,17 @@ public function setIsFilterableInGrid($isFilterableInGrid) $this->setData(self::IS_FILTERABLE_IN_GRID, $isFilterableInGrid); return $this; } + + /** + * @inheritDoc + */ + protected function _getDefaultBackendModel() + { + $backend = parent::_getDefaultBackendModel(); + if ($backend === Entity::DEFAULT_BACKEND_MODEL) { + $backend = DefaultBackend::class; + } + + return $backend; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php index 17ca389777c5b..c7c08bc805a1d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php @@ -33,7 +33,7 @@ public function build(int $productId, int $storeId) : array foreach ($this->linkedProductSelectBuilder as $productSelectBuilder) { $selects[] = $productSelectBuilder->build($productId, $storeId); } - $selects = array_merge(...$selects); + $selects = array_merge([], ...$selects); return $selects; } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml new file mode 100644 index 0000000000000..1799f6339a84d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + Checks for a subcategory in topmenu + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml new file mode 100644 index 0000000000000..73b0765394333 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml @@ -0,0 +1,19 @@ + + + + + + NonexistentProductSku + 1 + + + SecondNonexistentProductSku + 1 + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml index e1c8e5c75e9ac..15fcf5f7d4000 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml @@ -19,5 +19,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml index aff7ffe4d5763..1b041c5ca306f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml @@ -22,5 +22,6 @@ +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml new file mode 100644 index 0000000000000..a65d2c9e63bef --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml @@ -0,0 +1,14 @@ + + + +
+ + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index eebd3472cbd95..3008e89fd9dd1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -10,17 +10,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <description value="The test verifies that in Update on Schedule mode if displaying of category products on Storefront changes due to product properties change, the changes are NOT applied immediately, but applied only after cron runs (twice)."/> - <severity value="BLOCKER"/> - <testCaseId value="MC-11146"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26119"/> <group value="catalog"/> <group value="indexer"/> - <skip> - <issueId value="MC-20392"/> - </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php new file mode 100644 index 0000000000000..36ec38841b7cc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend; + +use Magento\Catalog\Model\AbstractModel; +use Magento\Catalog\Model\Attribute\Backend\DefaultBackend; +use Magento\Framework\DataObject; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; +use PHPUnit\Framework\TestCase; +use Magento\Eav\Model\Entity\Attribute as BasicAttribute; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; + +class DefaultBackendTest extends TestCase +{ + /** + * Different cases for attribute validation. + * + * @return array + */ + public function getAttributeConfigurations(): array + { + return [ + 'basic-attribute' => [true, false, true, 'basic', 'value', false, true, false], + 'non-html-attribute' => [false, false, false, 'non-html', 'value', false, false, false], + 'empty-html-attribute' => [false, false, true, 'html', null, false, true, false], + 'invalid-html-attribute' => [false, false, false, 'html', 'value', false, true, true], + 'valid-html-attribute' => [false, true, false, 'html', 'value', false, true, false], + 'changed-invalid-html-attribute' => [false, false, true, 'html', 'value', true, true, true], + 'changed-valid-html-attribute' => [false, true, true, 'html', 'value', true, true, false] + ]; + } + + /** + * Test attribute validation. + * + * @param bool $isBasic + * @param bool $isValidated + * @param bool $isCatalogEntity + * @param string $code + * @param mixed $value + * @param bool $isChanged + * @param bool $isHtmlAttribute + * @param bool $exceptionThrown + * @dataProvider getAttributeConfigurations + */ + public function testValidate( + bool $isBasic, + bool $isValidated, + bool $isCatalogEntity, + string $code, + $value, + bool $isChanged, + bool $isHtmlAttribute, + bool $exceptionThrown + ): void { + if ($isBasic) { + $attributeMock = $this->createMock(BasicAttribute::class); + } else { + $attributeMock = $this->createMock(Attribute::class); + $attributeMock->expects($this->any()) + ->method('getIsHtmlAllowedOnFront') + ->willReturn($isHtmlAttribute); + } + $attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($code); + + $validatorMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class); + if (!$isValidated) { + $validatorMock->expects($this->any()) + ->method('validate') + ->willThrowException(new ValidationException(__('HTML is invalid'))); + } else { + $validatorMock->expects($this->any())->method('validate'); + } + + if ($isCatalogEntity) { + $objectMock = $this->createMock(AbstractModel::class); + $objectMock->expects($this->any()) + ->method('getOrigData') + ->willReturn($isChanged ? $value .'-OLD' : $value); + } else { + $objectMock = $this->createMock(DataObject::class); + } + $objectMock->expects($this->any())->method('getData')->with($code)->willReturn($value); + + $model = new DefaultBackend($validatorMock); + $model->setAttribute($attributeMock); + + $actuallyThrownForSave = false; + try { + $model->beforeSave($objectMock); + } catch (AttributeException $exception) { + $actuallyThrownForSave = true; + } + $actuallyThrownForValidate = false; + try { + $model->validate($objectMock); + } catch (AttributeException $exception) { + $actuallyThrownForValidate = true; + } + $this->assertEquals($actuallyThrownForSave, $actuallyThrownForValidate); + $this->assertEquals($actuallyThrownForSave, $exceptionThrown); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php index 900f630a7434d..8274ed9da5f32 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Category as CategoryModel; use Magento\Catalog\Model\CategoryFactory; use Magento\Catalog\Model\CategoryRepository; +use Magento\Catalog\Model\CategoryRepository\PopulateWithValues; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityMetadata; @@ -63,6 +64,14 @@ class CategoryRepositoryTest extends TestCase */ protected $metadataPoolMock; + /** + * @var PopulateWithValues|MockObject + */ + private $populateWithValuesMock; + + /** + * @inheridoc + */ protected function setUp(): void { $this->categoryFactoryMock = $this->createPartialMock( @@ -94,6 +103,12 @@ protected function setUp(): void ->with(CategoryInterface::class) ->willReturn($metadataMock); + $this->populateWithValuesMock = $this + ->getMockBuilder(PopulateWithValues::class) + ->onlyMethods(['execute']) + ->disableOriginalConstructor() + ->getMock(); + $this->model = (new ObjectManager($this))->getObject( CategoryRepository::class, [ @@ -102,6 +117,7 @@ protected function setUp(): void 'storeManager' => $this->storeManagerMock, 'metadataPool' => $this->metadataPoolMock, 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverterMock, + 'populateWithValues' => $this->populateWithValuesMock, ] ); } @@ -202,7 +218,7 @@ public function testFilterExtraFieldsOnUpdateCategory($categoryId, $categoryData ->method('toNestedArray') ->willReturn($categoryData); $categoryMock->expects($this->once())->method('validate')->willReturn(true); - $categoryMock->expects($this->once())->method('addData')->with($dataForSave); + $this->populateWithValuesMock->expects($this->once())->method('execute')->with($categoryMock, $dataForSave); $this->categoryResourceMock->expects($this->once()) ->method('save') ->willReturn(DataObject::class); @@ -230,11 +246,11 @@ public function testCreateNewCategory() $categoryMock->expects($this->once())->method('getParentId')->willReturn($parentCategoryId); $parentCategoryMock->expects($this->once())->method('getPath')->willReturn('path'); - $categoryMock->expects($this->once())->method('addData')->with($dataForSave); $categoryMock->expects($this->once())->method('validate')->willReturn(true); $this->categoryResourceMock->expects($this->once()) ->method('save') ->willReturn(DataObject::class); + $this->populateWithValuesMock->expects($this->once())->method('execute')->with($categoryMock, $dataForSave); $this->assertEquals($categoryMock, $this->model->save($categoryMock)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php new file mode 100644 index 0000000000000..c5018f1aa6313 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Plugin; + +use Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; + +class TableResolverTest extends TestCase +{ + /** + * Tests replacing catalog_category_product_index table name + * + * @param int $storeId + * @param string $tableName + * @param string $expected + * @dataProvider afterGetTableNameDataProvider + */ + public function testAfterGetTableName(int $storeId, string $tableName, string $expected): void + { + $storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + + $storeMock = $this->getMockBuilder(Store::class) + ->onlyMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $storeMock->method('getId') + ->willReturn($storeId); + + $storeManagerMock->method('getStore')->willReturn($storeMock); + + $tableResolverMock = $this->getMockBuilder(IndexScopeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $tableResolverMock->method('resolve')->willReturn('catalog_category_product_index_store1'); + + $subjectMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $model = new TableResolver($storeManagerMock, $tableResolverMock); + + $this->assertEquals( + $expected, + $model->afterGetTableName($subjectMock, $tableName, 'catalog_category_product_index') + ); + } + + /** + * Data provider for testAfterGetTableName + * + * @return array + */ + public function afterGetTableNameDataProvider(): array + { + return [ + [ + 'storeId' => 1, + 'tableName' => 'catalog_category_product_index', + 'expected' => 'catalog_category_product_index_store1' + ], + [ + 'storeId' => 0, + 'tableName' => 'catalog_category_product_index', + 'expected' => 'catalog_category_product_index' + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php index c60ef266b7ebb..89243ea30c9dc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php @@ -62,7 +62,7 @@ protected function setUp(): void $this->closureMock = function () use ($productMock) { return $productMock; }; - $this->rollbackClosureMock = function () use ($productMock) { + $this->rollbackClosureMock = function () { throw new \Exception(self::ERROR_MSG); }; diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php index c449d0a2ba30b..675bdaa5f1db0 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php @@ -87,7 +87,7 @@ public function resolve( $this->tiers->addProductFilter($productId); return $this->valueFactory->create( - function () use ($productId, $context) { + function () use ($productId) { $tierPrices = $this->tiers->getProductTierPrices($productId); return $tierPrices ?? []; diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 105e91320de49..5fce0fcdf3ca2 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -155,6 +155,10 @@ function (AggregationValueInterface $value) { return []; } - return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes); + return $this->attributeOptionProvider->getOptions( + \array_merge([], ...$attributeOptionIds), + $storeId, + $attributes + ); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php index ff661236be62f..ac3f396b45ef8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php @@ -36,7 +36,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array foreach ($this->builders as $builder) { $layers[] = $builder->build($aggregation, $storeId); } - $layers = \array_merge(...$layers); + $layers = \array_merge([], ...$layers); return \array_filter($layers); } diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 0bfd9d58ec969..34f5dd831686c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -88,7 +88,7 @@ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): } } if ($fragmentFields) { - $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + $selectedFields = array_merge([], $selectedFields, ...$fragmentFields); } $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php index f709f8cd6eb72..8d584d15fff0e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php @@ -64,7 +64,7 @@ public function apply(Filter $filter, AbstractDb $collection) $collection->addCategoryFilter($category); } - $categoryProductIds = array_unique(array_merge(...$categoryProducts)); + $categoryProductIds = array_unique(array_merge([], ...$categoryProducts)); $collection->addIdFilter($categoryProductIds); return true; } diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php index 85fee62eb4303..4ea6b6bcfde9a 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -14,8 +12,6 @@ /** * Abstract action reindex class - * - * @package Magento\CatalogInventory\Model\Indexer\Stock */ abstract class AbstractAction { @@ -283,6 +279,8 @@ private function doReindex($productIds = []) } /** + * Get cache cleaner object + * * @return CacheCleaner */ private function getCacheCleaner() diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php index c7dfcffee3d31..9e5e39e4aeb53 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,8 +8,6 @@ /** * Class Row reindex action - * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action */ class Row extends \Magento\CatalogInventory\Model\Indexer\Stock\AbstractAction { diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php index f107955f0201e..a6176df3b107e 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,8 +8,6 @@ /** * Class Rows reindex action for mass actions - * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action */ class Rows extends \Magento\CatalogInventory\Model\Indexer\Stock\AbstractAction { diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index f1cef90fc68ca..005ffd11ac7a1 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php index 403f64e7f77f8..73c4a8833e433 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,7 +9,7 @@ class Processor extends \Magento\Framework\Indexer\AbstractProcessor { /** - * Indexer ID + * Get Indexer ID for cataloginventory_stock */ const INDEXER_ID = 'cataloginventory_stock'; } diff --git a/app/code/Magento/CatalogInventory/Model/StockIndex.php b/app/code/Magento/CatalogInventory/Model/StockIndex.php index ad0cff43c6ac9..6b659073485ad 100644 --- a/app/code/Magento/CatalogInventory/Model/StockIndex.php +++ b/app/code/Magento/CatalogInventory/Model/StockIndex.php @@ -169,11 +169,11 @@ protected function processChildren( $requiredChildrenIds = $typeInstance->getChildrenIds($productId, true); if ($requiredChildrenIds) { - $childrenIds = [[]]; + $childrenIds = []; foreach ($requiredChildrenIds as $groupedChildrenIds) { $childrenIds[] = $groupedChildrenIds; } - $childrenIds = array_merge(...$childrenIds); + $childrenIds = array_merge([], ...$childrenIds); $childrenWebsites = $this->productWebsite->getWebsites($childrenIds); foreach ($websitesWithStores as $websiteId => $storeId) { @@ -232,13 +232,13 @@ protected function getWebsitesWithDefaultStores($websiteId = null) */ protected function processParents($productId, $websiteId) { - $parentIds = [[]]; + $parentIds = []; foreach ($this->getProductTypeInstances() as $typeInstance) { /* @var ProductType\AbstractType $typeInstance */ $parentIds[] = $typeInstance->getParentIdsByChild($productId); } - $parentIds = array_merge(...$parentIds); + $parentIds = array_merge([], ...$parentIds); if (empty($parentIds)) { return; diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php index ca89ac01f280f..c888d522d2e8b 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php index 25b0c2ef33ebe..c9f60bd61c2fb 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php index e01f371b829d6..42d578ec88ea8 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php index 0e2b6b2f329c1..a81a4cd34b87f 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 5143762a07e08..ba58066cae917 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -359,6 +359,8 @@ protected function getPreparedSearchCriteria($attribute, $value) if (is_array($value)) { if (isset($value['from']) && isset($value['to'])) { if (!empty($value['from']) || !empty($value['to'])) { + $from = ''; + $to = ''; if (isset($value['currency'])) { /** @var $currencyModel Currency */ $currencyModel = $this->_currencyFactory->create()->load($value['currency']); diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index b1aecc6885bf0..080af5daa0322 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -70,7 +70,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) $label = $this->getOptionText($value); $labels[] = is_array($label) ? $label : [$label]; } - $label = implode(',', array_unique(array_merge(...$labels))); + $label = implode(',', array_unique(array_merge([], ...$labels))); $this->getLayer() ->getState() ->addFilter($this->_createItem($label, $attributeValue)); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php index 308b82e38c43a..50875b1a418d0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php @@ -121,7 +121,7 @@ public function afterSave( */ protected function generateProductUrls($websiteId, $originWebsiteId) { - $urls = [[]]; + $urls = []; $websiteIds = $websiteId != $originWebsiteId ? [$websiteId, $originWebsiteId] : [$websiteId]; @@ -136,7 +136,7 @@ protected function generateProductUrls($websiteId, $originWebsiteId) $urls[] = $this->productUrlRewriteGenerator->generate($product); } - return array_merge(...$urls); + return array_merge([], ...$urls); } /** @@ -148,7 +148,7 @@ protected function generateProductUrls($websiteId, $originWebsiteId) */ protected function generateCategoryUrls($rootCategoryId, $storeIds) { - $urls = [[]]; + $urls = []; $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true); foreach ($categories as $category) { /** @var \Magento\Catalog\Model\Category $category */ @@ -157,6 +157,6 @@ protected function generateCategoryUrls($rootCategoryId, $storeIds) $urls[] = $this->categoryUrlRewriteGenerator->generate($category); } - return array_merge(...$urls); + return array_merge([], ...$urls); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index b1dfa79373a05..b467771408ec0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -37,8 +37,6 @@ use RuntimeException; /** - * Class AfterImportDataObserver - * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -459,8 +457,7 @@ private function categoriesUrlRewriteGenerate(): array } } } - $result = !empty($urls) ? array_merge(...$urls) : []; - return $result; + return array_merge([], ...$urls); } /** diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 4c5cdca7ff126..7e6693ce68ef9 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -510,7 +510,7 @@ public function getPagerHtml() */ public function getIdentities() { - $identities = [[]]; + $identities = []; if ($this->getProductCollection()) { foreach ($this->getProductCollection() as $product) { if ($product instanceof IdentityInterface) { @@ -518,7 +518,7 @@ public function getIdentities() } } } - $identities = array_merge(...$identities); + $identities = array_merge([], ...$identities); return $identities ?: [Product::CACHE_TAG]; } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.xml new file mode 100644 index 0000000000000..dbc9739a9247f --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CheckoutSelectPurchaseOrderPaymentActionGroup"> + <annotations> + <description>Selects the 'Purchase Order' Payment Method on the Storefront Checkout page.</description> + </annotations> + + <arguments> + <argument name="purchaseOrderNumber" type="string"/> + </arguments> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <remove keyForRemoval="checkCheckMoneyOption"/> + <conditionalClick selector="{{CheckoutPaymentSection.purchaseOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.purchaseOrderPayment}}" visible="true" stepKey="checkPurchaseOrderOption"/> + <fillField selector="{{StorefrontCheckoutPaymentMethodSection.purchaseOrderNumber}}" userInput="{{purchaseOrderNumber}}" stepKey="fillPurchaseOrderNumber"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml index 60188224871eb..4b6680442a470 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml @@ -24,11 +24,12 @@ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForPageLoad stepKey="waitForShippingLoadingMask"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 1c9933064154a..100567d503c77 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -32,6 +32,7 @@ <element name="cartItemsArea" type="button" selector="div.block.items-in-cart"/> <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active" timeout="30"/> <element name="checkMoneyOrderPayment" type="radio" selector="input#checkmo.radio" timeout="30"/> + <element name="purchaseOrderPayment" type="radio" selector="input#purchaseorder.radio" timeout="30"/> <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> <element name="placeOrderWithoutTimeout" type="button" selector=".payment-method._active button.action.primary.checkout"/> <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[@data-role='title']" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml index 668d33d26f37a..1ecf97c50c81a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -39,6 +39,7 @@ <element name="emptyMiniCart" type="text" selector="//div[@class='minicart-wrapper']//span[@class='counter qty empty']/../.."/> <element name="minicartContent" type="block" selector="#minicart-content-wrapper"/> <element name="messageEmptyCart" type="text" selector="//*[@id='minicart-content-wrapper']//*[contains(@class,'subtitle empty')]"/> + <element name="emptyCartMessageContent" type="text" selector="#minicart-content-wrapper .minicart.empty.text" timeout="30"/> <element name="visibleItemsCountText" type="text" selector="//div[@class='items-total']"/> <element name="productQuantity" type="input" selector="//*[@id='mini-cart']//a[contains(text(),'{{productName}}')]/../..//div[@class='details-qty qty']//input[@data-item-qty='{{qty}}']" parameterized="true"/> <element name="productImage" type="text" selector="//ol[@id='mini-cart']//img[@class='product-image-photo']"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml index c4c70cef81b0b..e6a5f37c764fe 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml @@ -39,10 +39,10 @@ </after> <!-- Open product and add product to cart--> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> - <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> </actionGroup> <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> - <argument name="product" value="$$createProduct$$"/> + <argument name="product" value="$createProduct$"/> <argument name="productCount" value="1"/> </actionGroup> <!-- Go to cart --> @@ -59,5 +59,17 @@ <actualResult type="const">$grabCountry</actualResult> <expectedResult type="string">{{DE_Address_Berlin_Not_Default_Address.country_id}}</expectedResult> </assertEquals> + <!-- Go to cart --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="returnToCartPage"/> + <!-- Switch to default store view --> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefaultStoreView"/> + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="proceedToCheckoutWithDefaultStore"/> + <!-- Grab country code from checkout page and assert value with default country for default store view --> + <grabValueFrom selector="{{CheckoutShippingSection.country}}" stepKey="grabDefaultStoreCountry"/> + <assertEquals stepKey="assertDefaultCountryValue"> + <actualResult type="const">$grabDefaultStoreCountry</actualResult> + <expectedResult type="string">{{US_Address_TX.country_id}}</expectedResult> + </assertEquals> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml new file mode 100644 index 0000000000000..1055ff25edaef --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + + <test name="StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout with Purchase Order Payment. Create Order with Press Key Enter."/> + <title value="Create Checkout with purchase order payment method test. Press key Enter on field Purchase Order Number for create Order."/> + <description value="Create Checkout with purchase order payment method. Press key Enter on field Purchase Order Number for create Order."/> + <severity value="MAJOR"/> + <testCaseId value="MC-37227"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleTwo" stepKey="createSimpleProduct"/> + + <!-- Enable payment method --> + <magentoCLI command="config:set {{PurchaseOrderEnableConfigData.path}} {{PurchaseOrderEnableConfigData.value}}" stepKey="enablePaymentMethod"/> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!-- Disable payment method --> + <magentoCLI command="config:set {{PurchaseOrderDisabledConfigData.path}} {{PurchaseOrderDisabledConfigData.value}}" stepKey="disablePaymentMethod"/> + </after> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + + <!-- Checkout select Purchase Order payment --> + <actionGroup ref="CheckoutSelectPurchaseOrderPaymentActionGroup" stepKey="selectPurchaseOrderPayment"> + <argument name="purchaseOrderNumber" value="12345"/> + </actionGroup> + + <!--Press Key ENTER--> + <pressKey selector="{{StorefrontCheckoutPaymentMethodSection.purchaseOrderNumber}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressKeyEnter"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!--See success messages--> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order # is: " stepKey="seeOrderNumber"/> + + </test> + +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.xml new file mode 100644 index 0000000000000..0b46bbdb7db65 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckoutWithPurchaseOrderNumberTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout with Purchase Order Payment"/> + <title value="Create Checkout with purchase order payment method test"/> + <description value="Create Checkout with purchase order payment method"/> + <severity value="MAJOR"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleTwo" stepKey="createSimpleProduct"/> + + <!-- Enable payment method --> + <magentoCLI command="config:set {{PurchaseOrderEnableConfigData.path}} {{PurchaseOrderEnableConfigData.value}}" stepKey="enablePaymentMethod"/> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!-- Disable payment method --> + <magentoCLI command="config:set {{PurchaseOrderDisabledConfigData.path}} {{PurchaseOrderDisabledConfigData.value}}" stepKey="disablePaymentMethod"/> + </after> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + + <!-- Checkout select Purchase Order payment --> + <actionGroup ref="CheckoutSelectPurchaseOrderPaymentActionGroup" stepKey="selectPurchaseOrderPayment"> + <argument name="purchaseOrderNumber" value="12345"/> + </actionGroup> + + <!--Click Place Order button--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!--See success messages--> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order # is: " stepKey="seeOrderNumber"/> + + </test> + +</tests> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 646e6156ec646..2a52b64647749 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -121,7 +121,9 @@ define([ ); } checkoutProvider.on('shippingAddress', function (shippingAddrsData) { - checkoutData.setShippingAddressFromData(shippingAddrsData); + if (shippingAddrsData.street && !_.isEmpty(shippingAddrsData.street[0])) { + checkoutData.setShippingAddressFromData(shippingAddrsData); + } }); shippingRatesValidator.initFields(fieldsetName); }); diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php index 2643e69ba1efd..c78c807f9ea20 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php @@ -35,12 +35,12 @@ public function __construct($list = null) public function isValid($agreementIds = []) { $agreementIds = $agreementIds === null ? [] : $agreementIds; - $requiredAgreements = [[]]; + $requiredAgreements = []; foreach ($this->agreementsProviders as $agreementsProvider) { $requiredAgreements[] = $agreementsProvider->getRequiredAgreementIds(); } - $agreementsDiff = array_diff(array_merge(...$requiredAgreements), $agreementIds); + $agreementsDiff = array_diff(array_merge([], ...$requiredAgreements), $agreementIds); return empty($agreementsDiff); } diff --git a/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php b/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php new file mode 100644 index 0000000000000..e676cb1fe0ee5 --- /dev/null +++ b/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Command; + +use Magento\Cms\Model\Wysiwyg\Validator; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\App\Config\ConfigResource\ConfigInterface as ConfigWriter; +use Magento\Framework\App\Cache\TypeListInterface as Cache; + +/** + * Command to toggle WYSIWYG content validation on/off. + */ +class WysiwygRestrictCommand extends Command +{ + /** + * @var ConfigWriter + */ + private $configWriter; + + /** + * @var Cache + */ + private $cache; + + /** + * @param ConfigWriter $configWriter + * @param Cache $cache + */ + public function __construct(ConfigWriter $configWriter, Cache $cache) + { + parent::__construct(); + + $this->configWriter = $configWriter; + $this->cache = $cache; + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName('cms:wysiwyg:restrict'); + $this->setDescription('Set whether to enforce user HTML content validation or show a warning instead'); + $this->setDefinition([new InputArgument('restrict', InputArgument::REQUIRED, 'y\n')]); + + parent::configure(); + } + + /** + * @inheritDoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $restrictArg = mb_strtolower((string)$input->getArgument('restrict')); + $restrict = $restrictArg === 'y' ? '1' : '0'; + $this->configWriter->saveConfig(Validator::CONFIG_PATH_THROW_EXCEPTION, $restrict); + $this->cache->cleanType('config'); + + $output->writeln('HTML user content validation is now ' .($restrictArg === 'y' ? 'enforced' : 'suggested')); + + return 0; + } +} diff --git a/app/code/Magento/Cms/Model/Block.php b/app/code/Magento/Cms/Model/Block.php index 9da444c72e80c..ab8d65399f37c 100644 --- a/app/code/Magento/Cms/Model/Block.php +++ b/app/code/Magento/Cms/Model/Block.php @@ -6,8 +6,15 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Data\Collection\AbstractDb; /** * CMS block model @@ -40,6 +47,32 @@ class Block extends AbstractModel implements BlockInterface, IdentityInterface */ protected $_eventPrefix = 'cms_block'; + /** + * @var WYSIWYGValidatorInterface + */ + private $wysiwygValidator; + + /** + * @param Context $context + * @param Registry $registry + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data + * @param WYSIWYGValidatorInterface|null $wysiwygValidator + */ + public function __construct( + Context $context, + Registry $registry, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + array $data = [], + ?WYSIWYGValidatorInterface $wysiwygValidator = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); + $this->wysiwygValidator = $wysiwygValidator + ?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class); + } + /** * Construct. * @@ -63,12 +96,26 @@ public function beforeSave() } $needle = 'block_id="' . $this->getId() . '"'; - if (false == strstr($this->getContent(), (string) $needle)) { - return parent::beforeSave(); + if (strstr($this->getContent(), (string) $needle) !== false) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Make sure that static block content does not reference the block itself.') + ); } - throw new \Magento\Framework\Exception\LocalizedException( - __('Make sure that static block content does not reference the block itself.') - ); + parent::beforeSave(); + + //Validating HTML content. + if ($this->getContent() && $this->getContent() !== $this->getOrigData(self::CONTENT)) { + try { + $this->wysiwygValidator->validate($this->getContent()); + } catch (ValidationException $exception) { + throw new ValidationException( + __('Content field contains restricted HTML elements. %1', $exception->getMessage()), + $exception + ); + } + } + + return $this; } /** diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index d0df0d2b31caa..ef57f8ca7b849 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -12,14 +12,16 @@ use Magento\Cms\Model\ResourceModel\Block\CollectionFactory as BlockCollectionFactory; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\EntityManager\HydratorInterface; /** - * Class BlockRepository + * Default block repo impl. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class BlockRepository implements BlockRepositoryInterface @@ -69,6 +71,11 @@ class BlockRepository implements BlockRepositoryInterface */ private $collectionProcessor; + /** + * @var HydratorInterface + */ + private $hydrator; + /** * @param ResourceBlock $resource * @param BlockFactory $blockFactory @@ -79,6 +86,9 @@ class BlockRepository implements BlockRepositoryInterface * @param DataObjectProcessor $dataObjectProcessor * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor + * @param HydratorInterface|null $hydrator + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourceBlock $resource, @@ -89,7 +99,8 @@ public function __construct( DataObjectHelper $dataObjectHelper, DataObjectProcessor $dataObjectProcessor, StoreManagerInterface $storeManager, - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor = null, + ?HydratorInterface $hydrator = null ) { $this->resource = $resource; $this->blockFactory = $blockFactory; @@ -100,6 +111,7 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->storeManager = $storeManager; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->hydrator = $hydrator ?? ObjectManager::getInstance()->get(HydratorInterface::class); } /** @@ -115,6 +127,10 @@ public function save(Data\BlockInterface $block) $block->setStoreId($this->storeManager->getStore()->getId()); } + if ($block->getId() && $block instanceof Block && !$block->getOrigData()) { + $block = $this->hydrator->hydrate($this->getById($block->getId()), $this->hydrator->extract($block)); + } + try { $this->resource->save($block); } catch (\Exception $exception) { @@ -201,6 +217,7 @@ public function deleteById($blockId) */ private function getCollectionProcessor() { + //phpcs:disable Magento2.PHP.LiteralNamespaces if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( 'Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor' diff --git a/app/code/Magento/Cms/Model/Page.php b/app/code/Magento/Cms/Model/Page.php index 28d013f45f1fa..7e3e3ff44cfa0 100644 --- a/app/code/Magento/Cms/Model/Page.php +++ b/app/code/Magento/Cms/Model/Page.php @@ -13,6 +13,8 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; /** * Cms Page Model @@ -21,12 +23,13 @@ * @method Page setStoreId(int $storeId) * @method int getStoreId() * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ class Page extends AbstractModel implements PageInterface, IdentityInterface { /** - * No route page id + * Page ID for the 404 page. */ const NOROUTE_PAGE_ID = 'no-route'; @@ -64,6 +67,11 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface */ private $customLayoutRepository; + /** + * @var WYSIWYGValidatorInterface + */ + private $wysiwygValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -71,6 +79,7 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param CustomLayoutRepository|null $customLayoutRepository + * @param WYSIWYGValidatorInterface|null $wysiwygValidator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -78,11 +87,14 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - ?CustomLayoutRepository $customLayoutRepository = null + ?CustomLayoutRepository $customLayoutRepository = null, + ?WYSIWYGValidatorInterface $wysiwygValidator = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->customLayoutRepository = $customLayoutRepository ?? ObjectManager::getInstance()->get(CustomLayoutRepository::class); + $this->wysiwygValidator = $wysiwygValidator + ?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class); } /** @@ -594,6 +606,8 @@ private function validateNewIdentifier(): void /** * @inheritdoc * @since 101.0.0 + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function beforeSave() { @@ -615,6 +629,26 @@ public function beforeSave() $this->setData('layout_update_selected', $layoutUpdate); $this->customLayoutRepository->validateLayoutSelectedFor($this); + //Validating Content HTML. + $oldValue = null; + if ($this->getId()) { + if ($this->getOrigData()) { + $oldValue = $this->getOrigData(self::CONTENT); + } elseif (array_key_exists(self::CONTENT, $this->getStoredData())) { + $oldValue = $this->getStoredData()[self::CONTENT]; + } + } + if ($this->getContent() && $this->getContent() !== $oldValue) { + try { + $this->wysiwygValidator->validate($this->getContent()); + } catch (ValidationException $exception) { + throw new ValidationException( + __('Content HTML contains restricted elements. %1', $exception->getMessage()), + $exception + ); + } + } + return parent::beforeSave(); } diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 0439fbcd2f799..301c0efa740bd 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -17,6 +17,7 @@ use Magento\Framework\EntityManager\HydratorInterface; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Store\Model\StoreManagerInterface; @@ -133,15 +134,21 @@ public function __construct( private function validateLayoutUpdate(Data\PageInterface $page): void { //Persisted data - $savedPage = $page->getId() ? $this->getById($page->getId()) : null; + $oldData = null; + if ($page->getId() && $page instanceof Page) { + $oldData = $page->getOrigData(); + } //Custom layout update can be removed or kept as is. if ($page->getCustomLayoutUpdateXml() - && (!$savedPage || $page->getCustomLayoutUpdateXml() !== $savedPage->getCustomLayoutUpdateXml()) + && ( + !$oldData + || $page->getCustomLayoutUpdateXml() !== $oldData[Data\PageInterface::CUSTOM_LAYOUT_UPDATE_XML] + ) ) { throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); } if ($page->getLayoutUpdateXml() - && (!$savedPage || $page->getLayoutUpdateXml() !== $savedPage->getLayoutUpdateXml()) + && (!$oldData || $page->getLayoutUpdateXml() !== $oldData[Data\PageInterface::LAYOUT_UPDATE_XML]) ) { throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); } @@ -156,24 +163,28 @@ private function validateLayoutUpdate(Data\PageInterface $page): void */ public function save(\Magento\Cms\Api\Data\PageInterface $page) { - if ($page->getStoreId() === null) { - $storeId = $this->storeManager->getStore()->getId(); - $page->setStoreId($storeId); - } - $pageId = $page->getId(); - try { - $this->validateLayoutUpdate($page); - if ($pageId) { + $pageId = $page->getId(); + if ($pageId && !($page instanceof Page && $page->getOrigData())) { $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); } + if ($page->getStoreId() === null) { + $storeId = $this->storeManager->getStore()->getId(); + $page->setStoreId($storeId); + } + $this->validateLayoutUpdate($page); $this->resource->save($page); $this->identityMap->add($page); - } catch (\Exception $exception) { + } catch (LocalizedException $exception) { throw new CouldNotSaveException( __('Could not save the page: %1', $exception->getMessage()), $exception ); + } catch (\Throwable $exception) { + throw new CouldNotSaveException( + __('Could not save the page: %1', __('Something went wrong while saving the page.')), + $exception + ); } return $page; } diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php index 9fd94d4c11e1c..fe8817f5f40b4 100644 --- a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php +++ b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php @@ -11,6 +11,8 @@ use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\PageRepositoryInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\HydratorInterface; /** * Validates and saves a page @@ -27,13 +29,20 @@ class ValidationComposite implements PageRepositoryInterface */ private $validators; + /** + * @var HydratorInterface + */ + private $hydrator; + /** * @param PageRepositoryInterface $repository * @param ValidatorInterface[] $validators + * @param HydratorInterface|null $hydrator */ public function __construct( PageRepositoryInterface $repository, - array $validators = [] + array $validators = [], + ?HydratorInterface $hydrator = null ) { foreach ($validators as $validator) { if (!$validator instanceof ValidatorInterface) { @@ -44,6 +53,7 @@ public function __construct( } $this->repository = $repository; $this->validators = $validators; + $this->hydrator = $hydrator ?? ObjectManager::getInstance()->get(HydratorInterface::class); } /** @@ -51,6 +61,9 @@ public function __construct( */ public function save(PageInterface $page) { + if ($page->getId()) { + $page = $this->hydrator->hydrate($this->getById($page->getId()), $this->hydrator->extract($page)); + } foreach ($this->validators as $validator) { $validator->validate($page); } diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Validator.php b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php new file mode 100644 index 0000000000000..39360e6350967 --- /dev/null +++ b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Wysiwyg; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; +use Psr\Log\LoggerInterface; +use Magento\Framework\Message\Factory as MessageFactory; + +/** + * Processes backend validator results. + */ +class Validator implements WYSIWYGValidatorInterface +{ + public const CONFIG_PATH_THROW_EXCEPTION = 'cms/wysiwyg/force_valid'; + + /** + * @var WYSIWYGValidatorInterface + */ + private $validator; + + /** + * @var ManagerInterface + */ + private $messages; + + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var MessageFactory + */ + private $messageFactory; + + /** + * @param WYSIWYGValidatorInterface $validator + * @param ManagerInterface $messages + * @param ScopeConfigInterface $config + * @param LoggerInterface $logger + * @param MessageFactory $messageFactory + */ + public function __construct( + WYSIWYGValidatorInterface $validator, + ManagerInterface $messages, + ScopeConfigInterface $config, + LoggerInterface $logger, + MessageFactory $messageFactory + ) { + $this->validator = $validator; + $this->messages = $messages; + $this->config = $config; + $this->logger = $logger; + $this->messageFactory = $messageFactory; + } + + /** + * @inheritDoc + */ + public function validate(string $content): void + { + $throwException = $this->config->isSetFlag(self::CONFIG_PATH_THROW_EXCEPTION); + try { + $this->validator->validate($content); + } catch (ValidationException $exception) { + if ($throwException) { + throw $exception; + } else { + $this->messages->addUniqueMessages( + [ + $this->messageFactory->create( + MessageInterface::TYPE_WARNING, + (string)__( + 'Temporarily allowed to save HTML value that contains restricted elements. %1', + $exception->getMessage() + ) + ) + ] + ); + } + } catch (\Throwable $exception) { + if ($throwException) { + throw $exception; + } else { + $this->messages->addUniqueMessages( + [ + $this->messageFactory->create( + MessageInterface::TYPE_WARNING, + (string)__('Invalid HTML provided') + ) + ] + ); + $this->logger->error($exception); + } + } + } +} diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml index 1d5e8541dd497..f4e26938d9008 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> - <element name="image" type="file" selector="#tinymce img"/> + <element name="image" type="file" selector=".mce-content-body img"/> <element name="contentIframe" type="iframe" selector="cms_block_form_content_ifr"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php new file mode 100644 index 0000000000000..8e2fa44a24545 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\Wysiwyg; + +use Magento\Cms\Model\Wysiwyg\Validator; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Magento\Framework\Message\Factory as MessageFactory; + +class ValidatorTest extends TestCase +{ + /** + * Validation cases. + * + * @return array + */ + public function getValidationCases(): array + { + return [ + 'invalid-exception' => [true, new ValidationException(__('Invalid html')), true, false], + 'invalid-warning' => [false, new \RuntimeException('Invalid html'), false, true], + 'valid' => [false, null, false, false] + ]; + } + + /** + * Test validation. + * + * @param bool $isFlagSet + * @param \Throwable|null $thrown + * @param bool $exceptionThrown + * @param bool $warned + * @dataProvider getValidationCases + */ + public function testValidate(bool $isFlagSet, ?\Throwable $thrown, bool $exceptionThrown, bool $warned): void + { + $actuallyWarned = false; + + $messageFactoryMock = $this->createMock(MessageFactory::class); + $messageFactoryMock->method('create') + ->willReturnCallback( + function () { + return $this->getMockForAbstractClass(MessageInterface::class); + } + ); + $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $configMock->method('isSetFlag') + ->with(Validator::CONFIG_PATH_THROW_EXCEPTION) + ->willReturn($isFlagSet); + + $backendMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class); + if ($thrown) { + $backendMock->method('validate')->willThrowException($thrown); + } + + $messagesMock = $this->getMockForAbstractClass(ManagerInterface::class); + $messagesMock->method('addUniqueMessages') + ->willReturnCallback( + function () use (&$actuallyWarned): void { + $actuallyWarned = true; + } + ); + + $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + + $validator = new Validator($backendMock, $messagesMock, $configMock, $loggerMock, $messageFactoryMock); + try { + $validator->validate('content'); + $actuallyThrown = false; + } catch (\Throwable $exception) { + $actuallyThrown = true; + } + $this->assertEquals($exceptionThrown, $actuallyThrown); + $this->assertEquals($warned, $actuallyWarned); + } +} diff --git a/app/code/Magento/Cms/etc/config.xml b/app/code/Magento/Cms/etc/config.xml index 7090bb7a1fd25..d7a9e172f59a6 100644 --- a/app/code/Magento/Cms/etc/config.xml +++ b/app/code/Magento/Cms/etc/config.xml @@ -24,6 +24,7 @@ <wysiwyg> <enabled>enabled</enabled> <editor>mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter</editor> + <force_valid>0</force_valid> </wysiwyg> </cms> <system> diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 7fc8268eea5e0..2c265f881acf8 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -215,6 +215,7 @@ <type name="Magento\Cms\Model\BlockRepository"> <arguments> <argument name="collectionProcessor" xsi:type="object">Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor</argument> + <argument name="hydrator" xsi:type="object">Magento\Framework\EntityManager\AbstractModelHydrator</argument> </arguments> </type> @@ -234,6 +235,7 @@ <argument name="validators" xsi:type="array"> <item name="layout_update" xsi:type="object">Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator</item> </argument> + <argument name="hydrator" xsi:type="object">Magento\Framework\EntityManager\AbstractModelHydrator</argument> </arguments> </type> <preference for="Magento\Cms\Model\Page\CustomLayoutManagerInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutManager" /> @@ -243,4 +245,73 @@ </arguments> </type> <preference for="Magento\Cms\Model\Page\CustomLayoutRepositoryInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutRepository" /> + <type name="Magento\Cms\Model\Wysiwyg\Validator"> + <arguments> + <argument name="validator" xsi:type="object">DefaultWYSIWYGValidator</argument> + </arguments> + </type> + <preference for="Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface" type="Magento\Cms\Model\Wysiwyg\Validator" /> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="cms_wysiwyg_restrict" xsi:type="object">Magento\Cms\Command\WysiwygRestrictCommand</item> + </argument> + </arguments> + </type> + <virtualType name="DefaultWYSIWYGValidator" type="Magento\Framework\Validator\HTML\ConfigurableWYSIWYGValidator"> + <arguments> + <argument name="allowedTags" xsi:type="array"> + <item name="div" xsi:type="string">div</item> + <item name="a" xsi:type="string">a</item> + <item name="p" xsi:type="string">p</item> + <item name="span" xsi:type="string">span</item> + <item name="em" xsi:type="string">em</item> + <item name="strong" xsi:type="string">strong</item> + <item name="ul" xsi:type="string">ul</item> + <item name="li" xsi:type="string">li</item> + <item name="ol" xsi:type="string">ol</item> + <item name="h5" xsi:type="string">h5</item> + <item name="h4" xsi:type="string">h4</item> + <item name="h3" xsi:type="string">h3</item> + <item name="h2" xsi:type="string">h2</item> + <item name="h1" xsi:type="string">h1</item> + <item name="table" xsi:type="string">table</item> + <item name="tbody" xsi:type="string">tbody</item> + <item name="tr" xsi:type="string">tr</item> + <item name="td" xsi:type="string">td</item> + <item name="th" xsi:type="string">th</item> + <item name="tfoot" xsi:type="string">tfoot</item> + <item name="img" xsi:type="string">img</item> + <item name="hr" xsi:type="string">hr</item> + <item name="figure" xsi:type="string">figure</item> + <item name="button" xsi:type="string">button</item> + <item name="i" xsi:type="string">i</item> + <item name="u" xsi:type="string">u</item> + </argument> + <argument name="allowedAttributes" xsi:type="array"> + <item name="class" xsi:type="string">class</item> + <item name="width" xsi:type="string">width</item> + <item name="height" xsi:type="string">height</item> + <item name="style" xsi:type="string">style</item> + <item name="alt" xsi:type="string">alt</item> + <item name="title" xsi:type="string">title</item> + <item name="border" xsi:type="string">border</item> + <item name="id" xsi:type="string">id</item> + </argument> + <argument name="attributesAllowedByTags" xsi:type="array"> + <item name="a" xsi:type="array"> + <item name="href" xsi:type="string">href</item> + </item> + <item name="img" xsi:type="array"> + <item name="src" xsi:type="string">src</item> + </item> + <item name="button" xsi:type="array"> + <item name="type" xsi:type="string">type</item> + </item> + </argument> + <argument name="attributeValidators" xsi:type="array"> + <item name="style" xsi:type="object">Magento\Framework\Validator\HTML\StyleAttributeValidator</item> + </argument> + </arguments> + </virtualType> </config> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index d1c204c01ad1c..154e76bd93e41 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -11,14 +11,14 @@ $filters = $block->getConfig()->getFilters() ?? []; $allowedExtensions = []; $blockHtmlId = $block->getHtmlId(); -$listExtensions = [[]]; +$listExtensions = []; foreach ($filters as $media_type) { $listExtensions[] = array_map(function ($fileExt) { return ltrim($fileExt, '.*'); }, $media_type['files']); } -$allowedExtensions = array_merge(...$listExtensions); +$allowedExtensions = array_merge([], ...$listExtensions); $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() ? "{action: 'resize', maxWidth: " diff --git a/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml index 762d17bdf87f1..127677ce05e0d 100644 --- a/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml +++ b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml @@ -8,6 +8,7 @@ <suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> <suite name="AppConfigDumpSuite"> <before> + <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> <magentoCLI command="app:config:dump" stepKey="configDump"/> </before> <after> diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php new file mode 100644 index 0000000000000..9e1f3482d3c0f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; + +/** + * Filter configurable options by current store plugin. + */ +class UsedProductsWebsiteFilter +{ + /** + * Filter configurable options not assigned to current website. + * + * @param Configurable $subject + * @param ProductInterface $product + * @param array|null $requiredAttributeIds + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeGetUsedProducts( + Configurable $subject, + ProductInterface $product, + array $requiredAttributeIds = null + ): void { + $subject->setStoreFilter($product->getStore(), $product); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php index 56993ecec1fbf..75592efc52dca 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Model\Quote\Item; +use Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface; +use Magento\Quote\Api\Data\ProductOptionExtensionInterface; use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\Framework\Serialize\Serializer\Json; @@ -64,7 +66,7 @@ public function __construct( public function convertToBuyRequest(CartItemInterface $cartItem) { if ($cartItem->getProductOption() && $cartItem->getProductOption()->getExtensionAttributes()) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $options */ + /** @var ConfigurableItemOptionValueInterface $options */ $options = $cartItem->getProductOption()->getExtensionAttributes()->getConfigurableItemOptions(); if (is_array($options)) { $requestData = []; @@ -82,13 +84,17 @@ public function convertToBuyRequest(CartItemInterface $cartItem) */ public function processOptions(CartItemInterface $cartItem) { - $attributesOption = $cartItem->getProduct()->getCustomOption('attributes'); + $attributesOption = $cartItem->getProduct() + ->getCustomOption('attributes'); + if (!$attributesOption) { + return $cartItem; + } $selectedConfigurableOptions = $this->serializer->unserialize($attributesOption->getValue()); if (is_array($selectedConfigurableOptions)) { $configurableOptions = []; foreach ($selectedConfigurableOptions as $optionId => $optionValue) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $option */ + /** @var ConfigurableItemOptionValueInterface $option */ $option = $this->itemOptionValueFactory->create(); $option->setOptionId($optionId); $option->setOptionValue($optionValue); @@ -99,8 +105,8 @@ public function processOptions(CartItemInterface $cartItem) ? $cartItem->getProductOption() : $this->productOptionFactory->create(); - /** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */ - $extensibleAttribute = $productOption->getExtensionAttributes() + /** @var ProductOptionExtensionInterface $extensibleAttribute */ + $extensibleAttribute = $productOption->getExtensionAttributes() ? $productOption->getExtensionAttributes() : $this->extensionFactory->create(); @@ -108,6 +114,7 @@ public function processOptions(CartItemInterface $cartItem) $productOption->setExtensionAttributes($extensibleAttribute); $cartItem->setProductOption($productOption); } + return $cartItem; } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php index 10f5b1cbb344a..cd68e1dcfce24 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php @@ -59,7 +59,7 @@ class CartItemProcessorTest extends TestCase */ private $productOptionExtensionAttributes; - /** @var \PHPUnit\Framework\MockObject\MockObject */ + /** @var MockObject */ private $serializer; protected function setUp(): void @@ -263,4 +263,25 @@ public function testProcessProductOptionsIfOptionsExist() $this->assertEquals($cartItemMock, $this->model->processOptions($cartItemMock)); } + + /** + * Checks processOptions method with the empty custom option + * + * @return void + */ + public function testProcessProductWithEmptyOption(): void + { + $customOption = $this->createMock(Option::class); + $productMock = $this->createMock(Product::class); + $productMock->method('getCustomOption') + ->with('attributes') + ->willReturn(null); + $customOption->expects($this->never()) + ->method('getValue'); + $cartItemMock = $this->createPartialMock(Item::class, ['getProduct']); + $cartItemMock->expects($this->once()) + ->method('getProduct') + ->willReturn($productMock); + $this->assertEquals($cartItemMock, $this->model->processOptions($cartItemMock)); + } } diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index f60234453dc60..3942ec52cbb8b 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -12,5 +12,6 @@ </type> <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> <plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> + <plugin name="used_products_website_filter" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsWebsiteFilter" /> </type> </config> diff --git a/app/code/Magento/Csp/Helper/InlineUtil.php b/app/code/Magento/Csp/Helper/InlineUtil.php index f9dd9aafa459e..648ba51e34f7d 100644 --- a/app/code/Magento/Csp/Helper/InlineUtil.php +++ b/app/code/Magento/Csp/Helper/InlineUtil.php @@ -110,14 +110,14 @@ private function extractHost(string $url): ?string */ private function extractRemoteFonts(string $styleContent): array { - $urlsFound = [[]]; + $urlsFound = []; preg_match_all('/\@font\-face\s*?\{([^\}]*)[^\}]*?\}/im', $styleContent, $fontFaces); foreach ($fontFaces[1] as $fontFaceContent) { preg_match_all('/url\([\'\"]?(http(s)?\:[^\)]+)[\'\"]?\)/i', $fontFaceContent, $urls); $urlsFound[] = $urls[1]; } - return array_map([$this, 'extractHost'], array_merge(...$urlsFound)); + return array_map([$this, 'extractHost'], array_merge([], ...$urlsFound)); } /** diff --git a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php index d48df02d9de27..400aa56bc68e9 100644 --- a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php +++ b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php @@ -292,7 +292,7 @@ protected function _unserializeStoreConfig($configPath, $storeId = null) */ protected function getAllowedCurrencies() { - $allowedCurrencies = [[]]; + $allowedCurrencies = []; $allowedCurrencies[] = explode( self::ALLOWED_CURRENCIES_CONFIG_SEPARATOR, $this->_scopeConfig->getValue( @@ -330,6 +330,6 @@ protected function getAllowedCurrencies() } } } - return array_unique(array_merge(...$allowedCurrencies)); + return array_unique(array_merge([], ...$allowedCurrencies)); } } diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index a475a1b1af93d..ef2d2cca169f5 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -7,11 +7,16 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Framework\Api\ArrayObjectSearch; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\EncoderInterface; +use Magento\Framework\Locale\Bundle\DataBundle; +use Magento\Framework\Locale\ResolverInterface; /** * Customer date of birth attribute block * * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Dob extends AbstractWidget { @@ -39,6 +44,18 @@ class Dob extends AbstractWidget */ protected $filterFactory; + /** + * JSON Encoder + * + * @var EncoderInterface + */ + private $encoder; + + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Customer\Helper\Address $addressHelper @@ -46,6 +63,8 @@ class Dob extends AbstractWidget * @param \Magento\Framework\View\Element\Html\Date $dateElement * @param \Magento\Framework\Data\Form\FilterFactory $filterFactory * @param array $data + * @param EncoderInterface|null $encoder + * @param ResolverInterface|null $localeResolver */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -53,10 +72,14 @@ public function __construct( CustomerMetadataInterface $customerMetadata, \Magento\Framework\View\Element\Html\Date $dateElement, \Magento\Framework\Data\Form\FilterFactory $filterFactory, - array $data = [] + array $data = [], + ?EncoderInterface $encoder = null, + ?ResolverInterface $localeResolver = null ) { $this->dateElement = $dateElement; $this->filterFactory = $filterFactory; + $this->encoder = $encoder ?? ObjectManager::getInstance()->get(EncoderInterface::class); + $this->localeResolver = $localeResolver ?? ObjectManager::getInstance()->get(ResolverInterface::class); parent::__construct($context, $addressHelper, $customerMetadata, $data); } @@ -378,6 +401,32 @@ public function getFirstDay() ); } + /** + * Get translated calendar config json formatted + * + * @return string + */ + public function getTranslatedCalendarConfigJson(): string + { + $localeData = (new DataBundle())->get($this->localeResolver->getLocale()); + $monthsData = $localeData['calendar']['gregorian']['monthNames']; + $daysData = $localeData['calendar']['gregorian']['dayNames']; + + return $this->encoder->encode( + [ + 'closeText' => __('Done'), + 'prevText' => __('Prev'), + 'nextText' => __('Next'), + 'currentText' => __('Today'), + 'monthNames' => array_values(iterator_to_array($monthsData['format']['wide'])), + 'monthNamesShort' => array_values(iterator_to_array($monthsData['format']['abbreviated'])), + 'dayNames' => array_values(iterator_to_array($daysData['format']['wide'])), + 'dayNamesShort' => array_values(iterator_to_array($daysData['format']['abbreviated'])), + 'dayNamesMin' => array_values(iterator_to_array($daysData['format']['short'])), + ] + ); + } + /** * Set 2 places for day value in format string * diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index.php index 51dc39a2fc658..9595e473c1869 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index.php @@ -289,9 +289,8 @@ protected function prepareDefaultCustomerTitle(\Magento\Backend\Model\View\Resul protected function _addSessionErrorMessages($messages) { $messages = (array)$messages; - $session = $this->_getSession(); - $callback = function ($error) use ($session) { + $callback = function ($error) { if (!$error instanceof Error) { $error = new Error($error); } diff --git a/app/code/Magento/Customer/Model/Address/CompositeValidator.php b/app/code/Magento/Customer/Model/Address/CompositeValidator.php index 4c77f10c11de4..62308ba329d03 100644 --- a/app/code/Magento/Customer/Model/Address/CompositeValidator.php +++ b/app/code/Magento/Customer/Model/Address/CompositeValidator.php @@ -30,11 +30,11 @@ public function __construct( */ public function validate(AbstractAddress $address) { - $errors = [[]]; + $errors = []; foreach ($this->validators as $validator) { $errors[] = $validator->validate($address); } - return array_merge(...$errors); + return array_merge([], ...$errors); } } diff --git a/app/code/Magento/Customer/Model/Metadata/Form.php b/app/code/Magento/Customer/Model/Metadata/Form.php index 85637ebf508b8..81ded6dec071a 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form.php +++ b/app/code/Magento/Customer/Model/Metadata/Form.php @@ -363,11 +363,11 @@ public function validateData(array $data) { $validator = $this->_getValidator($data); if (!$validator->isValid(false)) { - $messages = [[]]; + $messages = []; foreach ($validator->getMessages() as $errorMessages) { $messages[] = (array)$errorMessages; } - return array_merge(...$messages); + return array_merge([], ...$messages); } return true; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php index 020067570efb4..1ca1c5622803f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php @@ -84,7 +84,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) $websiteIds = []; if (!$this->shareConfig->isGlobalScope()) { - $allowedCountries = [[]]; + $allowedCountries = []; foreach ($this->storeManager->getWebsites() as $website) { $countries = $this->allowedCountriesReader @@ -96,7 +96,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) } } - $allowedCountries = array_unique(array_merge(...$allowedCountries)); + $allowedCountries = array_unique(array_merge([], ...$allowedCountries)); } else { $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(); } diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml new file mode 100644 index 0000000000000..1f56ba505128f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontCustomerHasNoOtherAddressesActionGroup"> + <annotations> + <description>Verifies customer has no additional address in address book</description> + </annotations> + <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToAddressPage"/> + <waitForText userInput="You have no other address entries in your address book." selector=".block-addresses-list" stepKey="assertOtherAddresses"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 0e5eae1d47f1a..6da31b552c622 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -17,6 +17,7 @@ use Magento\Framework\Data\Form\FilterFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; @@ -91,6 +92,16 @@ class DobTest extends TestCase */ private $_locale; + /** + * @var EncoderInterface + */ + private $encoder; + + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * @inheritDoc */ @@ -110,14 +121,15 @@ protected function setUp(): void $cache->expects($this->any())->method('getFrontend')->willReturn($frontendCache); $objectManager = new ObjectManager($this); - $localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); - $localeResolver->expects($this->any()) + $this->localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->localeResolver->expects($this->any()) ->method('getLocale') ->willReturnCallback( function () { return $this->_locale; } ); + $localeResolver = $this->localeResolver; $timezone = $objectManager->getObject( Timezone::class, ['localeResolver' => $localeResolver, 'dateFormatterFactory' => new DateFormatterFactory()] @@ -157,12 +169,17 @@ function () use ($timezone, $localeResolver) { } ); + $this->encoder = $this->getMockForAbstractClass(EncoderInterface::class); + $this->_block = new Dob( $this->context, $this->createMock(Address::class), $this->customerMetadata, $this->createMock(Date::class), - $this->filterFactory + $this->filterFactory, + [], + $this->encoder, + $this->localeResolver ); } @@ -602,4 +619,80 @@ public function testGetHtmlExtraParamsWithRequiredOption() $this->_block->getHtmlExtraParams() ); } + + /** + * Tests getTranslatedCalendarConfigJson() + * + * @param string $locale + * @param array $expectedArray + * @param string $expectedJson + * @dataProvider getTranslatedCalendarConfigJsonDataProvider + * @return void + */ + public function testGetTranslatedCalendarConfigJson( + string $locale, + array $expectedArray, + string $expectedJson + ): void { + $this->_locale = $locale; + + $this->encoder->expects($this->once()) + ->method('encode') + ->with($expectedArray) + ->willReturn($expectedJson); + + $this->assertEquals( + $expectedJson, + $this->_block->getTranslatedCalendarConfigJson() + ); + } + + /** + * Provider for testGetTranslatedCalendarConfigJson + * + * @return array + */ + public function getTranslatedCalendarConfigJsonDataProvider() + { + return [ + [ + 'locale' => 'en_US', + 'expectedArray' => [ + 'closeText' => 'Done', + 'prevText' => 'Prev', + 'nextText' => 'Next', + 'currentText' => 'Today', + 'monthNames' => ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December'], + 'monthNamesShort' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + 'dayNames' => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + 'dayNamesShort' => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + 'dayNamesMin' => ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], + ], + // phpcs:disable Generic.Files.LineLength.TooLong + 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthNamesShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"dayNames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"dayNamesShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"dayNamesMin":["Su","Mo","Tu","We","Th","Fr","Sa"]}' + // phpcs:enable Generic.Files.LineLength.TooLong + ], + [ + 'locale' => 'de_DE', + 'expectedArray' => [ + 'closeText' => 'Done', + 'prevText' => 'Prev', + 'nextText' => 'Next', + 'currentText' => 'Today', + 'monthNames' => ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + 'monthNamesShort' => ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', + 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.'], + 'dayNames' => ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + 'dayNamesShort' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], + 'dayNamesMin' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], + ], + // phpcs:disable Generic.Files.LineLength.TooLong + 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],"monthNamesShort":["Jan.","Feb.","M\u00e4rz","Apr.","Mai","Juni","Juli","Aug.","Sept.","Okt.","Nov.","Dez."],"dayNames":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],"dayNamesShort":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],"dayNamesMin":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."]}' + // phpcs:enable Generic.Files.LineLength.TooLong + ], + ]; + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php index cd7154de14858..65f9b62b426c0 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php @@ -12,7 +12,6 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DataObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; class BillingTest extends TestCase { @@ -23,10 +22,7 @@ class BillingTest extends TestCase protected function setUp(): void { - $logger = $this->getMockBuilder(LoggerInterface::class) - ->getMock(); - /** @var LoggerInterface $logger */ - $this->testable = new Billing($logger); + $this->testable = new Billing(); } public function testBeforeSave() diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php index 3947a01582313..1f5485309cc19 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php @@ -12,7 +12,6 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DataObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; class ShippingTest extends TestCase { @@ -23,10 +22,7 @@ class ShippingTest extends TestCase protected function setUp(): void { - $logger = $this->getMockBuilder(LoggerInterface::class) - ->getMock(); - /** @var LoggerInterface $logger */ - $this->testable = new Shipping($logger); + $this->testable = new Shipping(); } public function testBeforeSave() diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php index c00b5cce02146..634b0d73219db 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php @@ -62,7 +62,7 @@ protected function setUp(): void $this->closureMock = function () use ($customerMock) { return $customerMock; }; - $this->rollbackClosureMock = function () use ($customerMock) { + $this->rollbackClosureMock = function () { throw new \Exception(self::ERROR_MSG); }; diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index 3c2f970faadee..da1c85cce9856 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** @var \Magento\Customer\Block\Widget\Dob $block */ /* @@ -23,14 +24,17 @@ NOTE: Regarding styles - if we leave it this way, we'll move it to boxes.css. Al automatically using block input parameters. */ +$translatedCalendarConfigJson = $block->getTranslatedCalendarConfigJson(); $fieldCssClass = 'field date field-' . $block->getHtmlId(); $fieldCssClass .= $block->isRequired() ? ' required' : ''; ?> <div class="<?= $block->escapeHtmlAttr($fieldCssClass) ?>"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('dob')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('dob')) ?></span> + </label> <div class="control customer-dob"> <?= $block->getFieldHtml() ?> - <?php if ($_message = $block->getAdditionalDescription()) : ?> + <?php if ($_message = $block->getAdditionalDescription()): ?> <div class="note"><?= $block->escapeHtml($_message) ?></div> <?php endif; ?> </div> @@ -42,4 +46,22 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; "Magento_Customer/js/validation": {} } } - </script> +</script> + +<?php $scriptString = <<<code + +require([ + 'jquery', + 'jquery-ui-modules/datepicker' +], function($){ + +//<![CDATA[ + $.extend(true, $, { + calendarConfig: {$translatedCalendarConfigJson} + }); +//]]> + +}); +code; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index f15f920fe95f4..0c3be73ec5047 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -525,12 +525,12 @@ public function validateData() protected function _importData() { //Preparing data for mass validation/import. - $rows = [[]]; + $rows = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $rows[] = $bunch; } - $this->prepareCustomerData(array_merge(...$rows)); + $this->prepareCustomerData(array_merge([], ...$rows)); unset($bunch, $rows); $this->_dataSourceModel->getIterator()->rewind(); diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 5ebf242bd6ac4..2a02205bdc7e5 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -514,8 +514,8 @@ protected function _importData() { while ($bunch = $this->_dataSourceModel->getNextBunch()) { $this->prepareCustomerData($bunch); - $entitiesToCreate = [[]]; - $entitiesToUpdate = [[]]; + $entitiesToCreate = []; + $entitiesToUpdate = []; $entitiesToDelete = []; $attributesToSave = []; @@ -549,8 +549,8 @@ protected function _importData() } } - $entitiesToCreate = array_merge(...$entitiesToCreate); - $entitiesToUpdate = array_merge(...$entitiesToUpdate); + $entitiesToCreate = array_merge([], ...$entitiesToCreate); + $entitiesToUpdate = array_merge([], ...$entitiesToUpdate); $this->updateItemsCounterStats($entitiesToCreate, $entitiesToUpdate, $entitiesToDelete); /** diff --git a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php index 8a75ad0def222..eb87a9c12125b 100644 --- a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php +++ b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php @@ -3,14 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Deploy\Console\Command\App; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; +use Magento\Deploy\Console\Command\App\ConfigImport\Processor; +use Magento\Framework\App\Area; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Magento\Framework\Console\Cli; -use Magento\Deploy\Console\Command\App\ConfigImport\Processor; /** * Runs the process of importing configuration data from shared source to appropriate application sources @@ -21,9 +28,6 @@ */ class ConfigImportCommand extends Command { - /** - * Command name. - */ const COMMAND_NAME = 'app:config:import'; /** @@ -33,12 +37,40 @@ class ConfigImportCommand extends Command */ private $processor; + /** + * @var EmulatedAdminhtmlAreaProcessor + */ + private $adminhtmlAreaProcessor; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var AreaList + */ + private $areaList; + /** * @param Processor $processor the configuration importer + * @param DeploymentConfig|null $deploymentConfig + * @param EmulatedAdminhtmlAreaProcessor|null $adminhtmlAreaProcessor + * @param AreaList|null $areaList */ - public function __construct(Processor $processor) - { + public function __construct( + Processor $processor, + DeploymentConfig $deploymentConfig = null, + EmulatedAdminhtmlAreaProcessor $adminhtmlAreaProcessor = null, + AreaList $areaList = null + ) { $this->processor = $processor; + $this->deploymentConfig = $deploymentConfig + ?? ObjectManager::getInstance()->get(DeploymentConfig::class); + $this->adminhtmlAreaProcessor = $adminhtmlAreaProcessor + ?? ObjectManager::getInstance()->get(EmulatedAdminhtmlAreaProcessor::class); + $this->areaList = $areaList + ?? ObjectManager::getInstance()->get(AreaList::class); parent::__construct(); } @@ -55,12 +87,26 @@ protected function configure() } /** - * Imports data from deployment configuration files to the DB. {@inheritdoc} + * Imports data from deployment configuration files to the DB. + * {@inheritdoc} + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output) { try { - $this->processor->execute($input, $output); + if ($this->canEmulateAdminhtmlArea()) { + // Emulate adminhtml area in order to execute all needed plugins declared only for this area + // For instance URL rewrite generation during creating store view + $this->adminhtmlAreaProcessor->process(function () use ($input, $output) { + $this->processor->execute($input, $output); + }); + } else { + $this->processor->execute($input, $output); + } } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); @@ -69,4 +115,19 @@ protected function execute(InputInterface $input, OutputInterface $output) return Cli::RETURN_SUCCESS; } + + /** + * Detects if we can emulate adminhtml area + * + * This area could be not available for instance during setup:install + * + * @return bool + * @throws RuntimeException + * @throws FileSystemException + */ + private function canEmulateAdminhtmlArea(): bool + { + return $this->deploymentConfig->isAvailable() + && in_array(Area::AREA_ADMINHTML, $this->areaList->getCodes()); + } } diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index 2a83d0d4c56ec..5780b46365680 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -443,11 +443,11 @@ public function getResultMap() */ public function getParentMap() { - $map = [[]]; + $map = []; foreach ($this->getParentPackages() as $parentPackage) { $map[] = $parentPackage->getMap(); } - return array_merge(...$map); + return array_merge([], ...$map); } /** @@ -458,7 +458,7 @@ public function getParentMap() */ public function getParentFiles($type = null) { - $files = [[]]; + $files = []; foreach ($this->getParentPackages() as $parentPackage) { if ($type === null) { $files[] = $parentPackage->getFiles(); @@ -466,7 +466,7 @@ public function getParentFiles($type = null) $files[] = $parentPackage->getFilesByType($type); } } - return array_merge(...$files); + return array_merge([], ...$files); } /** @@ -535,7 +535,7 @@ private function collectParentPaths( $area, $theme, $locale, - array & $result = [], + array &$result = [], ThemeInterface $themeModel = null ) { if (($package->getArea() != $area) || ($package->getTheme() != $theme) || ($package->getLocale() != $locale)) { diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php index da790a19f480a..32bdd63ef4638 100644 --- a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php @@ -7,8 +7,11 @@ namespace Magento\Deploy\Test\Unit\Console\Command\App; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; use Magento\Deploy\Console\Command\App\ConfigImport\Processor; use Magento\Deploy\Console\Command\App\ConfigImportCommand; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\RuntimeException; use PHPUnit\Framework\MockObject\MockObject; @@ -27,16 +30,37 @@ class ConfigImportCommandTest extends TestCase */ private $commandTester; + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfigMock; + + /** + * @var EmulatedAdminhtmlAreaProcessor|MockObject + */ + private $adminhtmlAreaProcessorMock; + + /** + * @var AreaList|MockObject + */ + private $areaListMock; + /** * @return void */ protected function setUp(): void { - $this->processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); + $this->processorMock = $this->createMock(Processor::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->adminhtmlAreaProcessorMock = $this->createMock(EmulatedAdminhtmlAreaProcessor::class); + $this->areaListMock = $this->createMock(AreaList::class); - $configImportCommand = new ConfigImportCommand($this->processorMock); + $configImportCommand = new ConfigImportCommand( + $this->processorMock, + $this->deploymentConfigMock, + $this->adminhtmlAreaProcessorMock, + $this->areaListMock + ); $this->commandTester = new CommandTester($configImportCommand); } @@ -46,6 +70,13 @@ protected function setUp(): void */ public function testExecute() { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->once()) + ->method('process')->willReturnCallback(function (callable $callback, array $params = []) { + return $callback(...$params); + }); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn(['adminhtml']); + $this->processorMock->expects($this->once()) ->method('execute'); @@ -57,6 +88,13 @@ public function testExecute() */ public function testExecuteWithException() { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->once()) + ->method('process')->willReturnCallback(function (callable $callback, array $params = []) { + return $callback(...$params); + }); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn(['adminhtml']); + $this->processorMock->expects($this->once()) ->method('execute') ->willThrowException(new RuntimeException(__('Some error'))); @@ -64,4 +102,34 @@ public function testExecuteWithException() $this->assertSame(Cli::RETURN_FAILURE, $this->commandTester->execute([])); $this->assertStringContainsString('Some error', $this->commandTester->getDisplay()); } + + /** + * @return void + */ + public function testExecuteWithDeploymentConfigNotAvailable() + { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(false); + $this->adminhtmlAreaProcessorMock->expects($this->never())->method('process'); + $this->areaListMock->expects($this->never())->method('getCodes'); + + $this->processorMock->expects($this->once()) + ->method('execute'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } + + /** + * @return void + */ + public function testExecuteWithMissingAdminhtmlLocale() + { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->never())->method('process'); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn([]); + + $this->processorMock->expects($this->once()) + ->method('execute'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } } diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index 0c32baebf12df..d40ed3144e7e6 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -35,6 +35,12 @@ </argument> </arguments> </type> + <type name="Magento\Deploy\Console\Command\App\ConfigImportCommand"> + <arguments> + <argument name="adminhtmlAreaProcessor" xsi:type="object">Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor\Proxy</argument> + <argument name="areaList" xsi:type="object">Magento\Framework\App\AreaList\Proxy</argument> + </arguments> + </type> <type name="Magento\Deploy\Model\Filesystem"> <arguments> <argument name="shell" xsi:type="object">Magento\Framework\App\Shell</argument> diff --git a/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php b/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php index 9e473ccaa2d92..8bd827958df15 100644 --- a/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php +++ b/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php @@ -116,7 +116,7 @@ private function getUrnDictionary(OutputInterface $output) $files = $this->filesUtility->getXmlCatalogFiles('*.xml'); $files = array_merge($files, $this->filesUtility->getXmlCatalogFiles('*.xsd')); - $urns = [[]]; + $urns = []; foreach ($files as $file) { // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileDir = dirname($file[0]); @@ -130,7 +130,7 @@ private function getUrnDictionary(OutputInterface $output) $urns[] = $matches[1]; } } - $urns = array_unique(array_merge(...$urns)); + $urns = array_unique(array_merge([], ...$urns)); $paths = []; foreach ($urns as $urn) { try { diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 204094571ba3b..c5eb27b21e58b 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -826,10 +826,9 @@ protected function _getAllItems() $fullItems[] = array_fill(0, $qty, $this->_getWeight($itemWeight)); } } - if ($fullItems) { - $fullItems = array_merge(...$fullItems); - sort($fullItems); - } + + $fullItems = array_merge([], ...$fullItems); + sort($fullItems); return $fullItems; } diff --git a/app/code/Magento/Directory/Model/AllowedCountries.php b/app/code/Magento/Directory/Model/AllowedCountries.php index 2ceeb70ba5b01..69326439edc03 100644 --- a/app/code/Magento/Directory/Model/AllowedCountries.php +++ b/app/code/Magento/Directory/Model/AllowedCountries.php @@ -62,11 +62,11 @@ public function getAllowedCountries( switch ($scope) { case ScopeInterface::SCOPE_WEBSITES: case ScopeInterface::SCOPE_STORES: - $allowedCountries = [[]]; + $allowedCountries = []; foreach ($scopeCode as $singleFilter) { $allowedCountries[] = $this->getCountriesFromConfig($this->getSingleScope($scope), $singleFilter); } - $allowedCountries = array_merge(...$allowedCountries); + $allowedCountries = array_merge([], ...$allowedCountries); break; default: $allowedCountries = $this->getCountriesFromConfig($scope, $scopeCode); diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index f7230df6e86ea..b574170ac5d3c 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -73,7 +73,7 @@ public function getConfigCurrencies(string $path) */ private function getConfigForAllStores(string $path) { - $storesResult = [[]]; + $storesResult = []; foreach ($this->storeManager->getStores() as $store) { $storesResult[] = explode( ',', @@ -81,7 +81,7 @@ private function getConfigForAllStores(string $path) ); } - return array_merge(...$storesResult); + return array_merge([], ...$storesResult); } /** diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php index 69f417e1ea732..f53f1e97a872d 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php @@ -152,7 +152,7 @@ protected function _prepareOptionValues( $inputType = ''; } - $values = [[]]; + $values = []; $isSystemAttribute = is_array($optionCollection); if ($isSystemAttribute) { $values[] = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); @@ -168,7 +168,7 @@ protected function _prepareOptionValues( } } - return array_merge(...$values); + return array_merge([], ...$values); } /** diff --git a/app/code/Magento/Eav/Model/Form.php b/app/code/Magento/Eav/Model/Form.php index 074c6cf46a2f4..b06c084cf6675 100644 --- a/app/code/Magento/Eav/Model/Form.php +++ b/app/code/Magento/Eav/Model/Form.php @@ -487,11 +487,11 @@ public function validateData(array $data) { $validator = $this->_getValidator($data); if (!$validator->isValid($this->getEntity())) { - $messages = [[]]; + $messages = []; foreach ($validator->getMessages() as $errorMessages) { $messages[] = (array)$errorMessages; } - return array_merge(...$messages); + return array_merge([], ...$messages); } return true; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Helper.php b/app/code/Magento/Eav/Model/ResourceModel/Helper.php index fc8a47994a6aa..c81db40c608a8 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Helper.php @@ -19,6 +19,7 @@ class Helper extends \Magento\Framework\DB\Helper * @param \Magento\Framework\App\ResourceConnection $resource * @param string $modulePrefix * @codeCoverageIgnore + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod */ public function __construct(\Magento\Framework\App\ResourceConnection $resource, $modulePrefix = 'Magento_Eav') { @@ -117,7 +118,7 @@ public function getLoadAttributesSelectGroups($selects) if (array_key_exists('all', $mainGroup)) { // it is better to call array_merge once after loop instead of calling it on each loop - $mainGroup['all'] = array_merge(...$mainGroup['all']); + $mainGroup['all'] = array_merge([], ...$mainGroup['all']); } return array_values($mainGroup); diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php index b276b67ff7fba..980842d6233b1 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php @@ -40,12 +40,12 @@ public function __construct(array $providers) */ public function getFields(array $context = []): array { - $allAttributes = [[]]; + $allAttributes = []; foreach ($this->providers as $provider) { $allAttributes[] = $provider->getFields($context); } - return array_merge(...$allAttributes); + return array_merge([], ...$allAttributes); } } diff --git a/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php b/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php index 197be38fb7f5f..8dc153f28c162 100644 --- a/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php @@ -49,6 +49,6 @@ public function getIdentities() if ($this->getItem()) { $identities[] = $this->getGroupedProduct()->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php b/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php index 97dc90ec93493..78ae4047c0aad 100644 --- a/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php @@ -32,10 +32,10 @@ protected function _getChildProducts() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getChildProducts() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 5ea6227231543..2f8bfdcf70a5e 100644 --- a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -242,7 +242,7 @@ public function getAllErrors() } $errors = array_values($this->items['rows']); - return array_merge(...$errors); + return array_merge([], ...$errors); } /** @@ -253,14 +253,14 @@ public function getAllErrors() */ public function getErrorsByCode(array $codes) { - $result = [[]]; + $result = []; foreach ($codes as $code) { if (isset($this->items['codes'][$code])) { $result[] = $this->items['codes'][$code]; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index e7517ba0c8818..735bc85244bdb 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -125,16 +125,16 @@ protected function getIndexers(InputInterface $input) return $indexers; } - $relatedIndexers = [[]]; - $dependentIndexers = [[]]; + $relatedIndexers = []; + $dependentIndexers = []; foreach ($indexers as $indexer) { $relatedIndexers[] = $this->getRelatedIndexerIds($indexer->getId()); $dependentIndexers[] = $this->getDependentIndexerIds($indexer->getId()); } - $relatedIndexers = $relatedIndexers ? array_unique(array_merge(...$relatedIndexers)) : []; - $dependentIndexers = $dependentIndexers ? array_merge(...$dependentIndexers) : []; + $relatedIndexers = array_unique(array_merge([], ...$relatedIndexers)); + $dependentIndexers = array_merge([], ...$dependentIndexers); $invalidRelatedIndexers = []; foreach ($relatedIndexers as $relatedIndexer) { @@ -165,12 +165,12 @@ protected function getIndexers(InputInterface $input) */ private function getRelatedIndexerIds(string $indexerId): array { - $relatedIndexerIds = [[]]; + $relatedIndexerIds = []; foreach ($this->getDependencyInfoProvider()->getIndexerIdsToRunBefore($indexerId) as $relatedIndexerId) { $relatedIndexerIds[] = [$relatedIndexerId]; $relatedIndexerIds[] = $this->getRelatedIndexerIds($relatedIndexerId); } - $relatedIndexerIds = $relatedIndexerIds ? array_unique(array_merge(...$relatedIndexerIds)) : []; + $relatedIndexerIds = array_unique(array_merge([], ...$relatedIndexerIds)); return $relatedIndexerIds; } @@ -183,7 +183,7 @@ private function getRelatedIndexerIds(string $indexerId): array */ private function getDependentIndexerIds(string $indexerId): array { - $dependentIndexerIds = [[]]; + $dependentIndexerIds = []; foreach (array_keys($this->getConfig()->getIndexers()) as $id) { $dependencies = $this->getDependencyInfoProvider()->getIndexerIdsToRunBefore($id); if (array_search($indexerId, $dependencies) !== false) { @@ -191,7 +191,7 @@ private function getDependentIndexerIds(string $indexerId): array $dependentIndexerIds[] = $this->getDependentIndexerIds($id); } } - $dependentIndexerIds = $dependentIndexerIds ? array_unique(array_merge(...$dependentIndexerIds)) : []; + $dependentIndexerIds = array_unique(array_merge([], ...$dependentIndexerIds)); return $dependentIndexerIds; } diff --git a/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php b/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php index 2d323fea34e7d..b6ea810666b9b 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php @@ -222,13 +222,13 @@ public function isTreeEmpty() */ protected function _getAllResourceIds(array $resources) { - $resourceIds = [[]]; + $resourceIds = []; foreach ($resources as $resource) { $resourceIds[] = [$resource['id']]; if (isset($resource['children'])) { $resourceIds[] = $this->_getAllResourceIds($resource['children']); } } - return array_merge(...$resourceIds); + return array_merge([], ...$resourceIds); } } diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 1ea2dc2618778..942741e9f7975 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -10,6 +10,8 @@ use Magento\Quote\Model\Quote\Address; use Magento\Checkout\Helper\Data as CheckoutHelper; use Magento\Framework\App\ObjectManager; +use Magento\Quote\Model\Quote\Address\Total\Collector; +use Magento\Store\Model\ScopeInterface; /** * Multishipping checkout overview information @@ -429,9 +431,12 @@ public function getBillingAddressTotals() */ public function renderTotals($totals, $colspan = null) { - //check if the shipment is multi shipment + // check if the shipment is multi shipment $totals = $this->getMultishippingTotals($totals); + // sort totals by configuration settings + $totals = $this->sortTotals($totals); + if ($colspan === null) { $colspan = 3; } @@ -481,4 +486,38 @@ protected function _getRowItemRenderer($type) } return $renderer; } + + /** + * Sort total information based on configuration settings. + * + * @param array $totals + * @return array + */ + private function sortTotals($totals): array + { + $sortedTotals = []; + $sorts = $this->_scopeConfig->getValue( + Collector::XML_PATH_SALES_TOTALS_SORT, + ScopeInterface::SCOPE_STORES + ); + + $sorted = []; + foreach ($sorts as $code => $sortOrder) { + $sorted[$sortOrder] = $code; + } + ksort($sorted); + + foreach ($sorted as $code) { + if (isset($totals[$code])) { + $sortedTotals[$code] = $totals[$code]; + } + } + + $notSorted = array_diff(array_keys($totals), array_keys($sortedTotals)); + foreach ($notSorted as $code) { + $sortedTotals[$code] = $totals[$code]; + } + + return $sortedTotals; + } } diff --git a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php index 7da77030f308a..2d044afd32c70 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php @@ -8,6 +8,7 @@ namespace Magento\Multishipping\Test\Unit\Block\Checkout; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; @@ -67,6 +68,11 @@ class OverviewTest extends TestCase */ private $urlBuilderMock; + /** + * @var MockObject + */ + private $scopeConfigMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -85,6 +91,7 @@ protected function setUp(): void $this->createMock(Multishipping::class); $this->quoteMock = $this->createMock(Quote::class); $this->urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); + $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->model = $objectManager->getObject( Overview::class, [ @@ -92,7 +99,8 @@ protected function setUp(): void 'totalsCollector' => $this->totalsCollectorMock, 'totalsReader' => $this->totalsReaderMock, 'multishipping' => $this->checkoutMock, - 'urlBuilder' => $this->urlBuilderMock + 'urlBuilder' => $this->urlBuilderMock, + '_scopeConfig' => $this->scopeConfigMock ] ); } @@ -187,4 +195,44 @@ public function testGetVirtualProductEditUrl() $this->urlBuilderMock->expects($this->once())->method('getUrl')->with('checkout/cart', [])->willReturn($url); $this->assertEquals($url, $this->model->getVirtualProductEditUrl()); } + + /** + * Test sort total information + * + * @return void + */ + public function testSortCollectors(): void + { + $sorts = [ + 'discount' => 40, + 'subtotal' => 10, + 'tax' => 20, + 'shipping' => 30, + ]; + + $this->scopeConfigMock->method('getValue') + ->with('sales/totals_sort', 'stores') + ->willReturn($sorts); + + $totalsNotSorted = [ + 'subtotal' => [], + 'shipping' => [], + 'tax' => [], + ]; + + $totalsExpected = [ + 'subtotal' => [], + 'tax' => [], + 'shipping' => [], + ]; + + $method = new \ReflectionMethod($this->model, 'sortTotals'); + $method->setAccessible(true); + $result = $method->invoke($this->model, $totalsNotSorted); + + $this->assertEquals( + $totalsExpected, + $result + ); + } } diff --git a/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html b/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html index 89d16bd732e7c..3a42a84b620b8 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html +++ b/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html @@ -42,27 +42,29 @@ </div> </div> </fieldset> - </form> - <div class="checkout-agreements-block"> - <!-- ko foreach: $parent.getRegion('before-place-order') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - </div> - <div class="actions-toolbar" id="review-buttons-container"> - <div class="primary"> - <button class="action primary checkout" - type="submit" - data-bind=" - click: placeOrder, - attr: {title: $t('Place Order')}, - enable: (getCode() == isChecked()), - css: {disabled: !isPlaceOrderActionAllowed()} - " - data-role="review-save"> - <span data-bind="i18n: 'Place Order'"></span> - </button> + + <div class="checkout-agreements-block"> + <!-- ko foreach: $parent.getRegion('before-place-order') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> </div> - </div> + + <div class="actions-toolbar" id="review-buttons-container"> + <div class="primary"> + <button class="action primary checkout" + type="submit" + data-bind=" + click: placeOrder, + attr: {title: $t('Place Order')}, + enable: (getCode() == isChecked()), + css: {disabled: !isPlaceOrderActionAllowed()} + " + data-role="review-save"> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </form> </div> </div> - + diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index bbc199c91263a..112accbae8070 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -140,10 +140,9 @@ public function collectRates(RateRequest $request) $freePackageValue += $item->getBaseRowTotal(); } } - $oldValue = $request->getPackageValue(); - $newPackageValue = $oldValue - $freePackageValue; - $request->setPackageValue($newPackageValue); - $request->setPackageValueWithDiscount($newPackageValue); + + $request->setPackageValue($request->getPackageValue() - $freePackageValue); + $request->setPackageValueWithDiscount($request->getPackageValueWithDiscount() - $freePackageValue); } if (!$request->getConditionName()) { diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml new file mode 100644 index 0000000000000..d225e5fa28f97 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?><!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest"> + <annotations> + <features value="Shipping"/> + <stories value="Offline Shipping Methods"/> + <title value="SalesRule Discount Is Applied On PackageValue For TableRate"/> + <description value="SalesRule Discount Is Applied On PackageValue For TableRate"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-38271"/> + <group value="shipping"/> + </annotations> + <before> + <!-- Add simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">13.00</field> + </createData> + + <!-- Create cart price rule --> + <createData entity="ActiveSalesRuleForNotLoggedIn" stepKey="createCartPriceRule"/> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Enable Table Rate method and save config --> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + + <!-- Uncheck Use Default checkbox for Default Condition --> + <uncheckOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" stepKey="disableUseDefaultCondition"/> + + <!-- Make sure you have Condition Price vs. Destination --> + <selectOption selector="{{AdminShippingMethodTableRatesSection.condition}}" userInput="{{TableRateShippingMethodConfig.package_value_with_discount}}" stepKey="setCondition"/> + + <!-- Import file and save config --> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="usa_tablerates.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + </before> + <after> + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Check Use Default checkbox for Default Condition and Active --> + <checkOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" stepKey="enableUseDefaultCondition"/> + <checkOption selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="enableUseDefaultActive"/> + + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Remove simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete sales rule --> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Assert that table rate value is correct for US --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCheckout"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <waitForPageLoad stepKey="waitForSelectCountry"/> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$5.99" stepKey="seeShippingForUS"/> + + <!-- Apply Coupon --> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyDiscount"> + <argument name="coupon" value="$$createCouponForCartPriceRule$$"/> + </actionGroup> + + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$7.99" stepKey="seeShippingForUSWithDiscount"/> + </test> +</tests> diff --git a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php index 1b64f3b635c03..6aff8aef2c2d9 100644 --- a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php +++ b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php @@ -84,7 +84,7 @@ public function afterGenerateElements(Layout $subject) public function afterGetOutput(Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { - $tags = [[]]; + $tags = []; $isVarnish = $this->config->getType() === Config::VARNISH; foreach ($subject->getAllBlocks() as $block) { @@ -96,7 +96,7 @@ public function afterGetOutput(Layout $subject, $result) $tags[] = $block->getIdentities(); } } - $tags = array_unique(array_merge(...$tags)); + $tags = array_unique(array_merge([], ...$tags)); $tags = $this->pageCacheTagsPreprocessor->process($tags); $this->response->setHeader('X-Magento-Tags', implode(',', $tags)); } diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index f5e25ce36e973..1e51b6e110728 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -57,8 +57,8 @@ sub vcl_recv { return (pass); } - # Bypass shopping cart, checkout and search requests - if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") { + # Bypass shopping cart and checkout + if (req.url ~ "/checkout") { return (pass); } @@ -121,13 +121,6 @@ sub vcl_hash { hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); } - # For multi site configurations to not cache each other's content - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - if (req.url ~ "/graphql") { call process_graphql_headers; } diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 92bb3394486fc..7adededf33036 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -58,8 +58,8 @@ sub vcl_recv { return (pass); } - # Bypass shopping cart, checkout and search requests - if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") { + # Bypass shopping cart and checkout + if (req.url ~ "/checkout") { return (pass); } @@ -122,13 +122,6 @@ sub vcl_hash { hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); } - # For multi site configurations to not cache each other's content - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - # To make sure http users don't see ssl warning if (req.http./* {{ ssl_offloaded_header }} */) { hash_data(req.http./* {{ ssl_offloaded_header }} */); diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index b23bec4c45fb8..bce89fe263573 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -62,8 +62,8 @@ sub vcl_recv { return (pass); } - # Bypass shopping cart, checkout and search requests - if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") { + # Bypass shopping cart and checkout + if (req.url ~ "/checkout") { return (pass); } @@ -126,13 +126,6 @@ sub vcl_hash { hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); } - # For multi site configurations to not cache each other's content - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - # To make sure http users don't see ssl warning if (req.http./* {{ ssl_offloaded_header }} */) { hash_data(req.http./* {{ ssl_offloaded_header }} */); diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index 8c8d13300849e..af42554484117 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -59,8 +59,8 @@ public function __construct( public function validate(array $validationSubject) { $isValid = true; - $failsDescriptionAggregate = [[]]; - $errorCodesAggregate = [[]]; + $failsDescriptionAggregate = []; + $errorCodesAggregate = []; foreach ($this->validators as $key => $validator) { $result = $validator->validate($validationSubject); if (!$result->isValid()) { @@ -76,8 +76,8 @@ public function validate(array $validationSubject) return $this->createResult( $isValid, - array_merge(...$failsDescriptionAggregate), - array_merge(...$errorCodesAggregate) + array_merge([], ...$failsDescriptionAggregate), + array_merge([], ...$errorCodesAggregate) ); } } diff --git a/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php b/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php index 61410499e956e..a3cef539dc17b 100644 --- a/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php +++ b/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php @@ -84,7 +84,7 @@ public function modify(array $initialStructure) */ private function getMoveInstructions($section, $data) { - $moved = [[]]; + $moved = []; if (array_key_exists('children', $data)) { foreach ($data['children'] as $childSection => $childData) { @@ -106,6 +106,6 @@ private function getMoveInstructions($section, $data) ]; } - return array_merge(...$moved); + return array_merge([], ...$moved); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php index dc54b71324a9b..a6a18418e92ac 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php @@ -162,14 +162,14 @@ public function testMovedToTargetSpecialGroup() */ private function fetchAllAvailableGroups($structure) { - $availableGroups = [[]]; + $availableGroups = []; foreach ($structure as $group => $data) { $availableGroups[] = [$group]; if (isset($data['children'])) { $availableGroups[] = $this->fetchAllAvailableGroups($data['children']); } } - $availableGroups = array_merge(...$availableGroups); + $availableGroups = array_merge([], ...$availableGroups); $availableGroups = array_values(array_unique($availableGroups)); sort($availableGroups); return $availableGroups; diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php index 13b19e4f79c9a..7e8b4d916334f 100644 --- a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -56,6 +56,6 @@ public function build(CartItem $cartItem): DataObject $requestData[] = $provider->execute($cartItem); } - return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php index c14cc1324732c..c4909eef31287 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -45,11 +45,11 @@ public function __construct( */ public function build(array $cartItemData): DataObject { - $requestData = [[]]; + $requestData = []; foreach ($this->providers as $provider) { $requestData[] = $provider->execute($cartItemData); } - return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } } diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index e14d8bde6be74..fac7b23d408e3 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -89,8 +89,7 @@ private function findRelations(array $products, array $loadAttributes, int $link if (!$relations) { return []; } - $relatedIds = array_values($relations); - $relatedIds = array_unique(array_merge(...$relatedIds)); + $relatedIds = array_unique(array_merge([], ...array_values($relations))); //Loading products data. $this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in'); $relatedSearchResult = $this->productDataProvider->getList( @@ -142,7 +141,7 @@ public function resolve(ContextInterface $context, Field $field, array $requests $products[] = $request->getValue()['model']; $fields[] = $this->productFieldsSelector->getProductFieldsFromInfo($request->getInfo(), $this->getNode()); } - $fields = array_unique(array_merge(...$fields)); + $fields = array_unique(array_merge([], ...$fields)); //Finding relations. $related = $this->findRelations($products, $fields, $this->getLinkType()); diff --git a/app/code/Magento/Reports/Block/Product/Viewed.php b/app/code/Magento/Reports/Block/Product/Viewed.php index ba4d03182213a..09d59e475905b 100644 --- a/app/code/Magento/Reports/Block/Product/Viewed.php +++ b/app/code/Magento/Reports/Block/Product/Viewed.php @@ -76,10 +76,10 @@ protected function _toHtml() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getItemsCollection() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php index efef617acf900..81f670de91805 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php @@ -68,7 +68,7 @@ public function getItem() */ public function getOrderOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -80,7 +80,7 @@ public function getOrderOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View.php index e4b12c30e71b4..d70df80038193 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_Sales * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php index cbb79f188f231..57fc0441fe830 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php @@ -39,7 +39,7 @@ public function getOrder() */ public function getItemOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getOrderItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -52,7 +52,7 @@ public function getItemOptions() } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php index 0291a1275c350..cb9c7315244ac 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php @@ -34,7 +34,7 @@ public function getOrder() */ public function getItemOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -47,7 +47,7 @@ public function getItemOptions() } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index bca6d49760d9a..010878559c2f0 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -105,7 +105,7 @@ public function getOrderItem() */ public function getItemOptions() { - $result = [[]]; + $result = []; $options = $this->getOrderItem()->getProductOptions(); if ($options) { if (isset($options['options'])) { @@ -118,7 +118,7 @@ public function getItemOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 393d61b69bf22..80e0ce168d7f5 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -550,6 +550,9 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) $quote = $this->getQuote(); if (!$quote->isVirtual() && $this->getShippingAddress()->getSameAsBilling()) { + $quote->getBillingAddress()->setCustomerAddressId( + $quote->getShippingAddress()->getCustomerAddressId() + ); $this->setShippingAsBilling(1); } @@ -2120,6 +2123,9 @@ private function isAddressesAreEqual(Order $order) $billingData['address_type'], $billingData['entity_id'] ); + if (isset($shippingData['customer_address_id']) && !isset($billingData['customer_address_id'])) { + unset($shippingData['customer_address_id']); + } return $shippingData == $billingData; } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index 93c8ed00f9daa..a92a1480bd023 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -111,6 +111,12 @@ public function send( 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index 004f36c277028..44b4df17619d8 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -111,6 +111,12 @@ public function send( 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php index 29e011217ef20..a7315aeb9e3be 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php @@ -326,7 +326,7 @@ public function getItemPricesForDisplay() */ public function getItemOptions() { - $result = [[]]; + $result = []; $options = $this->getItem()->getOrderItem()->getProductOptions(); if ($options) { if (isset($options['options'])) { @@ -339,7 +339,7 @@ public function getItemOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index fe68555d9f7c7..286d33815aea1 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -112,7 +112,13 @@ public function send( 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php b/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php index 645e411b80b67..10b3ca1bde996 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php @@ -37,11 +37,11 @@ public function __construct(TMapFactory $tmapFactory, array $providers = []) */ public function getIds($mainTableName, $gridTableName) { - $result = [[]]; + $result = []; foreach ($this->providers as $provider) { $result[] = $provider->getIds($mainTableName, $gridTableName); } - return array_unique(array_merge(...$result)); + return array_unique(array_merge([], ...$result)); } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminStartReorderFromOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminStartReorderFromOrderPageActionGroup.xml new file mode 100644 index 0000000000000..28a179faff9ac --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminStartReorderFromOrderPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminStartReorderFromOrderPageActionGroup"> + <annotations> + <description>Reorder existing order. Requires admin order page to be opened.</description> + </annotations> + + <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> + <waitForPageLoad stepKey="waitPageLoad"/> + <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForPageTitle"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeCreateNewOrderPageTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml new file mode 100644 index 0000000000000..1c3ab70857151 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReorderAddressNotSavedInAddressBookTest"> + <annotations> + <features value="Sales"/> + <stories value="Reorder"/> + <title value="Same shipping address is not repeating multiple times in storefront checkout when Reordered"/> + <description value="Same shipping address is not repeating multiple times in storefront checkout when Reordered"/> + <testCaseId value="MC-38412"/> + <useCaseId value="MC-38113"/> + <severity value="MAJOR"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="category"/> + <createData entity="ApiSimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_Customer_Without_Address" stepKey="customer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$customer$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Create order for registered customer --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$product$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="openCheckoutPage"/> + <actionGroup ref="LoggedInUserCheckoutFillingShippingSectionActionGroup" stepKey="fillAddressForm"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Reorder created order --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrderById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <actionGroup ref="AdminStartReorderFromOrderPageActionGroup" stepKey="startReorder"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + + <!-- Assert no additional addresses saved --> + <actionGroup ref="AssertStorefrontCustomerHasNoOtherAddressesActionGroup" stepKey="assertAddresses"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 99e00a74f1ba3..8bc739e9c68fd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -273,6 +273,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -283,6 +288,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index eaf57ad1bfc56..4a909a21e2558 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -272,6 +272,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -282,6 +287,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 81ed71ae7bb67..713b38f7d7f4a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -284,6 +284,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -296,6 +301,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new DataObject($transport); diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index dc007e4801b41..12927dcf526a3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -114,7 +114,9 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if (!$block->getDontSaveInAddressBook()): ?> checked="checked"<?php endif; ?> + <?php if (!$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + checked="checked" + <?php endif; ?> class="admin__control-checkbox"/> <label for="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" class="admin__field-label"><?= $block->escapeHtml(__('Save in address book')) ?></label> diff --git a/app/code/Magento/Search/Model/Autocomplete.php b/app/code/Magento/Search/Model/Autocomplete.php index 45957e8795744..57364e4c36bde 100644 --- a/app/code/Magento/Search/Model/Autocomplete.php +++ b/app/code/Magento/Search/Model/Autocomplete.php @@ -30,11 +30,11 @@ public function __construct( */ public function getItems() { - $data = [[]]; + $data = []; foreach ($this->dataProviders as $dataProvider) { $data[] = $dataProvider->getItems(); } - return array_merge(...$data); + return array_merge([], ...$data); } } diff --git a/app/code/Magento/Search/Model/SynonymGroupRepository.php b/app/code/Magento/Search/Model/SynonymGroupRepository.php index dbc2b66b1f047..c670235d67adb 100644 --- a/app/code/Magento/Search/Model/SynonymGroupRepository.php +++ b/app/code/Magento/Search/Model/SynonymGroupRepository.php @@ -150,7 +150,7 @@ private function create(SynonymGroupInterface $synonymGroup, $errorOnMergeConfli */ private function merge(SynonymGroupInterface $synonymGroupToMerge, array $matchingGroupIds) { - $mergedSynonyms = [[]]; + $mergedSynonyms = []; foreach ($matchingGroupIds as $groupId) { /** @var SynonymGroup $synonymGroupModel */ $synonymGroupModel = $this->synonymGroupFactory->create(); @@ -160,7 +160,7 @@ private function merge(SynonymGroupInterface $synonymGroupToMerge, array $matchi } $mergedSynonyms[] = explode(',', $synonymGroupToMerge->getSynonymGroup()); - return array_unique(array_merge(...$mergedSynonyms)); + return array_unique(array_merge([], ...$mergedSynonyms)); } /** diff --git a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php index 618d941f7047e..edb572dfdd4d1 100644 --- a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php @@ -6,10 +6,6 @@ namespace Magento\SendFriend\Model\ResourceModel; /** - * SendFriend Log Resource Model - * - * @author Magento Core Team <core@magentocommerce.com> - * * @api * @since 100.0.2 */ @@ -32,6 +28,7 @@ protected function _construct() * @param int $ip * @param int $startTime * @param int $websiteId + * * @return int * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -46,7 +43,7 @@ public function getSendCount($object, $ip, $startTime, $websiteId = null) AND time>=:time AND website_id=:website_id' ); - $bind = ['ip' => ip2long($ip), 'time' => $startTime, 'website_id' => (int)$websiteId]; + $bind = ['ip' => $ip, 'time' => $startTime, 'website_id' => (int)$websiteId]; $row = $connection->fetchRow($select, $bind); return $row['count']; @@ -58,14 +55,16 @@ public function getSendCount($object, $ip, $startTime, $websiteId = null) * @param int $ip * @param int $startTime * @param int $websiteId + * * @return $this */ public function addSendItem($ip, $startTime, $websiteId) { $this->getConnection()->insert( $this->getMainTable(), - ['ip' => ip2long($ip), 'time' => $startTime, 'website_id' => $websiteId] + ['ip' => $ip, 'time' => $startTime, 'website_id' => $websiteId] ); + return $this; } @@ -73,6 +72,7 @@ public function addSendItem($ip, $startTime, $websiteId) * Delete Old logs * * @param int $time + * * @return $this */ public function deleteLogsBefore($time) diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml index 47ef68cc9d765..ceae9c546bd3b 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml @@ -16,4 +16,10 @@ <data key="title">Best Way</data> <data key="methodName">Table Rate</data> </entity> + <!-- Set Table Rate Shipping method Condition --> + <entity name="TableRateShippingMethodConfig" type="shipping_method"> + <data key="package_weight">Weight vs. Destination</data> + <data key="package_value_with_discount">Price vs. Destination</data> + <data key="package_qty"># of Items vs. Destination</data> + </entity> </entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index 188b12c6a91c3..0c0372850a3c4 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -13,13 +13,14 @@ <features value="Configuration"/> <stories value="Disable configuration inputs"/> <title value="Check that all input fields disabled after executing CLI app:config:dump"/> - <description value="Check that all input fields disabled after executing CLI app:config:dump"/> + <description value="Check that all input fields disabled after executing CLI app:config:dump. Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again."/> <severity value="MAJOR"/> <testCaseId value="MC-11158"/> <useCaseId value="MAGETWO-96428"/> <group value="configuration"/> </annotations> <before> + <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml index 206deb0f5c795..6b188c21056e5 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml @@ -50,7 +50,11 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : } }); packaging.setItemQtyCallback(function(itemId){ - var item = $$('[name="shipment[items]['+itemId+']"]')[0]; + var item = $$('[name="shipment[items]['+itemId+']"]')[0], + itemTitle = $('order_item_' + itemId + '_title'); + if (!itemTitle && !item) { + return 0; + } if (item && !isNaN(item.value)) { return item.value; } diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml new file mode 100644 index 0000000000000..604ef606e94e5 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddVisualSwatchToProductWithOutCreatedActionGroup"> + <annotations> + <description>Does not create an attribute. Adds the provided Visual Swatch Attribute and Options (2) to a Product on the Admin Product creation/edit page. Clicks on Save. Validates that the Success Message is present. </description> + </annotations> + <arguments> + <argument name="attribute" defaultValue="visualSwatchAttribute"/> + </arguments> + + <seeInCurrentUrl url="{{ProductCatalogPage.url}}" stepKey="seeOnProductEditPage"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPanel"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="waitForSlideOut"/> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{attribute.default_label}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(attribute.default_label)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..5722210abf211 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Click a swatch option on product page--> + <actionGroup name="StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup"> + <arguments> + <argument name="productId" type="string"/> + <argument name="visualSwatchOptionLabel" type="string" /> + </arguments> + <click selector="{{StorefrontCategoryPageProductInfoSection.visualSwatchOption(productId,visualSwatchOptionLabel)}}" stepKey="clickSwatchOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.xml new file mode 100644 index 0000000000000..5f321c7f17603 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryPageProductInfoSection"> + <element name="visualSwatchOption" type="button" selector="#product-item-info_{{var1}} .swatch-option[data-option-label='{{var2}}']" parameterized="true"/> + <element name="productAddToWishlist" type="button" selector="#product-item-info_{{var1}} .action.towishlist" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php index 7863b70f6626a..d34e863d56c54 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php @@ -206,7 +206,7 @@ public function getOptionRates() { $size = self::TAX_RULES_CHUNK_SIZE; $page = 1; - $rates = [[]]; + $rates = []; do { $offset = $size * ($page - 1); $this->getSelect()->reset(); @@ -222,6 +222,6 @@ public function getOptionRates() $page++; } while ($this->getSize() > $offset); - return array_merge(...$rates); + return array_merge([], ...$rates); } } diff --git a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php index e528f9e88d8a4..c998c02d46b3c 100644 --- a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php +++ b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php @@ -94,17 +94,17 @@ public function getPageLayoutsConfig() protected function getConfigFiles() { if (!$this->configFiles) { - $configFiles = []; $this->configFiles = $this->cacheModel->load(self::CACHE_KEY_LAYOUTS); if (!empty($this->configFiles)) { //if value in cache is corrupted. $this->configFiles = $this->serializer->unserialize($this->configFiles); } if (empty($this->configFiles)) { + $configFiles = []; foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { $configFiles[] = $this->fileCollector->getFilesContent($theme, 'layouts.xml'); } - $this->configFiles = array_merge(...$configFiles); + $this->configFiles = array_merge([], ...$configFiles); $this->cacheModel->save($this->serializer->serialize($this->configFiles), self::CACHE_KEY_LAYOUTS); } } diff --git a/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php b/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php index 65e2b934741ee..b7f2def1c0fbd 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php @@ -7,10 +7,7 @@ namespace Magento\Theme\Test\Unit\Block\Adminhtml\Design\Config\Edit; -use Magento\Backend\Block\Widget\Context; -use Magento\Framework\UrlInterface; use Magento\Theme\Block\Adminhtml\Design\Config\Edit\SaveButton; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SaveButtonTest extends TestCase @@ -20,16 +17,9 @@ class SaveButtonTest extends TestCase */ protected $block; - /** - * @var Context|MockObject - */ - protected $context; - protected function setUp(): void { - $this->initContext(); - - $this->block = new SaveButton($this->context); + $this->block = new SaveButton(); } public function testGetButtonData() @@ -41,18 +31,4 @@ public function testGetButtonData() $this->assertArrayHasKey('data_attribute', $result); $this->assertIsArray($result['data_attribute']); } - - protected function initContext() - { - $this->urlBuilder = $this->getMockBuilder(UrlInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->context = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); - $this->context->expects($this->any()) - ->method('getUrlBuilder') - ->willReturn($this->urlBuilder); - } } diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 8eaac4aa3e794..bf76933b356c0 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -8,6 +8,7 @@ <page layout="3columns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="default_head_blocks"/> <body> + <attribute name="id" value="html-body"/> <block name="require.js" class="Magento\Framework\View\Element\Template" template="Magento_Theme::page/js/require_js.phtml" /> <referenceContainer name="after.body.start"> <block class="Magento\RequireJs\Block\Html\Head\Config" name="requirejs-config"/> diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index b6e539bdadcb9..4d0e3efee35aa 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1135,7 +1135,7 @@ protected function _getXmlTracking($trackings) </TrackRequest> XMLAuth; - $trackingResponses[] = $this->asyncHttpClient->request( + $trackingResponses[$tracking] = $this->asyncHttpClient->request( new Request( $url, Request::METHOD_POST, @@ -1144,13 +1144,9 @@ protected function _getXmlTracking($trackings) ) ); } - foreach ($trackingResponses as $response) { + foreach ($trackingResponses as $tracking => $response) { $httpResponse = $response->get(); - if ($httpResponse->getStatusCode() >= 400) { - $xmlResponse = ''; - } else { - $xmlResponse = $httpResponse->getBody(); - } + $xmlResponse = $httpResponse->getStatusCode() >= 400 ? '' : $httpResponse->getBody(); $this->_parseXmlTrackingResponse($tracking, $xmlResponse); } @@ -1362,10 +1358,11 @@ public function getAllowedMethods() protected function _formShipmentRequest(DataObject $request) { $packages = $request->getPackages(); + $shipmentItems = []; foreach ($packages as $package) { $shipmentItems[] = $package['items']; } - $shipmentItems = array_merge(...$shipmentItems); + $shipmentItems = array_merge([], ...$shipmentItems); $xmlRequest = $this->_xmlElFactory->create( ['data' => '<?xml version = "1.0" ?><ShipmentConfirmRequest xml:lang="en-US"/>'] @@ -1528,24 +1525,18 @@ protected function _formShipmentRequest(DataObject $request) } if ($deliveryConfirmation && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_PACKAGE) { - $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); - $serviceOptionsNode->addChild( - 'DeliveryConfirmation' - )->addChild( - 'DCISType', - $deliveryConfirmation - ); + $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); + $serviceOptionsNode + ->addChild('DeliveryConfirmation') + ->addChild('DCISType', $deliveryConfirmation); } } if (isset($deliveryConfirmation) && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_SHIPMENT) { $serviceOptionsNode = $shipmentPart->addChild('ShipmentServiceOptions'); - $serviceOptionsNode->addChild( - 'DeliveryConfirmation' - )->addChild( - 'DCISType', - $deliveryConfirmation - ); + $serviceOptionsNode + ->addChild('DeliveryConfirmation') + ->addChild('DCISType', $deliveryConfirmation); } $shipmentPart->addChild('PaymentInformation') @@ -1627,6 +1618,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) try { $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); } catch (Throwable $e) { + $response = $this->_xmlElFactory->create(['data' => '']); $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; } @@ -1800,6 +1792,7 @@ protected function _doShipmentRequest(DataObject $request) $this->setXMLAccessRequest(); $xmlRequest = $this->_xmlAccessRequest . $rawXmlRequest; $xmlResponse = $this->_getCachedQuotes($xmlRequest); + $debugData = []; if ($xmlResponse === null) { $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index f38c0f0978536..5ead1beb722dd 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -33,10 +33,8 @@ class Generator extends AbstractSchemaGenerator */ const ERROR_SCHEMA = '#/definitions/error-response'; - /** Unauthorized description */ const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized'; - /** Array signifier */ const ARRAY_SIGNIFIER = '[0]'; /** @@ -758,7 +756,7 @@ private function handleComplex($name, $type, $prefix, $isArray) ); } - return empty($queryNames) ? [] : array_merge(...$queryNames); + return array_merge([], ...$queryNames); } /** diff --git a/app/code/Magento/Weee/Model/Total/Quote/Weee.php b/app/code/Magento/Weee/Model/Total/Quote/Weee.php index 449c6cd688668..e7ae84c15a51f 100644 --- a/app/code/Magento/Weee/Model/Total/Quote/Weee.php +++ b/app/code/Magento/Weee/Model/Total/Quote/Weee.php @@ -306,12 +306,12 @@ protected function getNextIncrement() */ protected function recalculateParent(AbstractItem $item) { - $associatedTaxables = [[]]; + $associatedTaxables = []; foreach ($item->getChildren() as $child) { $associatedTaxables[] = $child->getAssociatedTaxables(); } $item->setAssociatedTaxables( - array_unique(array_merge(...$associatedTaxables)) + array_unique(array_merge([], ...$associatedTaxables)) ); } diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml new file mode 100644 index 0000000000000..6d17a5c687b1a --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml @@ -0,0 +1,19 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveAndContinueWidgetActionGroup"> + <annotations> + <description>Click on the Save an Continue button and check the success message</description> + </annotations> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewWidgetSection.saveAndContinue}}" stepKey="clickSaveWidget"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageAppeared"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml new file mode 100644 index 0000000000000..ce19c1b086328 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetWidgetNameAndStoreActionGroup"> + <annotations> + <description>Set widget name, store IDs and sort order on Widget edit page</description> + </annotations> + <arguments> + <argument name="widgetTitle" defaultValue="{{ProductsListWidget.name}}" type="string"/> + <argument name="widgetStoreIds" defaultValue="{{ProductsListWidget.store_ids}}" type="string"/> + <argument name="widgetSortOrder" defaultValue="{{ProductsListWidget.sort_order}}" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="waitForWidgetTitleInputVisible"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widgetTitle}}" stepKey="fillTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" parameterArray="{{widgetStoreIds}}" stepKey="setWidgetStoreId"/> + <fillField selector="{{AdminNewWidgetSection.widgetSortOrder}}" userInput="{{widgetSortOrder}}" stepKey="fillSortOrder"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetTypeAndDesignActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetTypeAndDesignActionGroup.xml new file mode 100644 index 0000000000000..3a9b4c53572c7 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetTypeAndDesignActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetWidgetTypeAndDesignActionGroup"> + <annotations> + <description>Select type and design on Widget edit page</description> + </annotations> + <arguments> + <argument name="widgetType" defaultValue="{{ProductsListWidget.type}}" type="string"/> + <argument name="widgetDesign" defaultValue="{{ProductsListWidget.design_theme}}" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminNewWidgetSection.widgetType}}" stepKey="waitForTypeInputVisible"/> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widgetType}}" stepKey="setWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widgetDesign}}" stepKey="setWidgetDesignTheme"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml index 5c7dd8fc54f20..bb739aa88b1f0 100644 --- a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml @@ -12,6 +12,7 @@ <data key="type">Catalog Products List</data> <data key="design_theme">Magento Luma</data> <data key="name" unique="suffix">TestWidget</data> + <data key="sort_order">0</data> <array key="store_ids"> <item>All Store Views</item> </array> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index f267ebe968a08..8a17b589d7ab2 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -12,6 +12,7 @@ <element name="widgetType" type="select" selector="#code"/> <element name="widgetDesignTheme" type="select" selector="#theme_id"/> <element name="continue" type="button" timeout="30" selector="#continue_button"/> + <element name="resetBtn" type="button" selector=".page-actions-buttons button#reset" timeout="30"/> <element name="widgetTitle" type="input" selector="#title"/> <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> @@ -38,13 +39,14 @@ <element name="searchBlock" type="button" selector="//div[@class='admin__filter-actions']/button[@title='Search']"/> <element name="blockStatus" type="select" selector="//select[@name='chooser_is_active']"/> <element name="searchedBlock" type="button" selector="//*[@class='magento-message']//tbody/tr/td[1]"/> - <element name="saveWidget" type="select" selector="#save"/> + <element name="saveWidget" type="button" selector="#save" timeout="30"/> <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> <element name="specificEntitySelectContainer" type="select" selector="select[name='widget_instance[0][anchor_categories][block]']"/> <element name="specificEntitySelectRadio" type="input" selector="#specific_anchor_categories_0"/> <element name="specificEntityOptionsChooser" type="button" selector="#anchor_categories_ids_0 .widget-option-chooser"/> + <element name="widgetInstanceType" type="select" selector=".admin__field-control select#instance_code" /> <!-- Catalog Product List Widget Options --> <element name="title" type="input" selector="[name='parameters[title]']"/> <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> @@ -52,3 +54,4 @@ <element name="cacheLifetime" type="input" selector="[name='parameters[cache_lifetime]']"/> </section> </sections> + diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml new file mode 100644 index 0000000000000..5e053778fe7ed --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminResetWidgetTest"> + <annotations> + <features value="Widget"/> + <stories value="CMS Widgets"/> + <title value="Reset Widget"/> + <description value="Check that admin user can reset widget form after filling out all information"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37892"/> + <group value="widget"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteWidget"> + <argument name="widget" value="ProductsListWidget"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="amOnAdminNewWidgetPage"/> + <actionGroup ref="AdminSetWidgetTypeAndDesignActionGroup" stepKey="firstSetTypeAndDesign"> + <argument name="widgetType" value="{{ProductsListWidget.type}}"/> + <argument name="widgetDesign" value="{{ProductsListWidget.design_theme}}"/> + </actionGroup> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetInstance"/> + <dontSeeInField userInput="{{ProductsListWidget.type}}" selector="{{AdminNewWidgetSection.widgetType}}" stepKey="dontSeeTypeAfterReset"/> + <dontSeeInField userInput="{{ProductsListWidget.design_theme}}" selector="{{AdminNewWidgetSection.widgetDesignTheme}}" stepKey="dontSeeDesignAfterReset"/> + <actionGroup ref="AdminSetWidgetTypeAndDesignActionGroup" stepKey="setTypeAndDesignAfterReset"> + <argument name="widgetType" value="{{ProductsListWidget.type}}"/> + <argument name="widgetDesign" value="{{ProductsListWidget.design_theme}}"/> + </actionGroup> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> + <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStore"> + <argument name="widgetTitle" value="{{ProductsListWidget.name}}"/> + <argument name="widgetStoreIds" value="{{ProductsListWidget.store_ids}}"/> + <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> + </actionGroup> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetNameAndStore"/> + <dontSeeInField userInput="{{ProductsListWidget.name}}" selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="dontSeeNameAfterReset"/> + <dontSeeInField userInput="{{ProductsListWidget.store_ids[0]}}" selector="{{AdminNewWidgetSection.widgetStoreIds}}" stepKey="dontSeeStoreAfterReset"/> + <dontSeeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="dontSeeSortOrderAfterReset"/> + <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStoreAfterReset"> + <argument name="widgetTitle" value="{{ProductsListWidget.name}}"/> + <argument name="widgetStoreIds" value="{{ProductsListWidget.store_ids}}"/> + <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> + </actionGroup> + <actionGroup ref="AdminSaveAndContinueWidgetActionGroup" stepKey="saveWidgetAndContinue"/> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetWidgetForm"/> + <seeInField userInput="{{ProductsListWidget.name}}" selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="seeNameAfterReset"/> + <seeInField userInput="{{ProductsListWidget.store_ids[0]}}" selector="{{AdminNewWidgetSection.widgetStoreIds}}" stepKey="seeStoreAfterReset"/> + <seeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="seeSortOrderAfterReset"/> + <seeInField userInput="{{ProductsListWidget.type}}" selector="{{AdminNewWidgetSection.widgetInstanceType}}" stepKey="seeTypeAfterReset"/> + <seeInField userInput="{{ProductsListWidget.design_theme}}" selector="{{AdminNewWidgetSection.widgetDesignTheme}}" stepKey="seeThemeAfterReset"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Block/AddToWishlist.php b/app/code/Magento/Wishlist/Block/AddToWishlist.php index 3ba350af94176..7997a6ed99031 100644 --- a/app/code/Magento/Wishlist/Block/AddToWishlist.php +++ b/app/code/Magento/Wishlist/Block/AddToWishlist.php @@ -6,13 +6,19 @@ namespace Magento\Wishlist\Block; +use Magento\Catalog\Api\Data\ProductTypeInterface; +use Magento\Catalog\Api\ProductTypeListInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; + /** * Wishlist js plugin initialization block * * @api * @since 100.1.0 */ -class AddToWishlist extends \Magento\Framework\View\Element\Template +class AddToWishlist extends Template { /** * Product types @@ -22,17 +28,25 @@ class AddToWishlist extends \Magento\Framework\View\Element\Template private $productTypes; /** - * @param \Magento\Framework\View\Element\Template\Context $context + * @var ProductTypeListInterface + */ + private $productTypeList; + + /** + * AddToWishlist constructor. + * + * @param Context $context * @param array $data + * @param ProductTypeListInterface|null $productTypeList */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, - array $data = [] + Context $context, + array $data = [], + ?ProductTypeListInterface $productTypeList = null ) { - parent::__construct( - $context, - $data - ); + parent::__construct($context, $data); + $this->productTypes = []; + $this->productTypeList = $productTypeList ?: ObjectManager::getInstance()->get(ProductTypeListInterface::class); } /** @@ -49,36 +63,16 @@ public function getWishlistOptions() /** * Returns an array of product types * - * @return array|null - * @throws \Magento\Framework\Exception\LocalizedException + * @return array */ - private function getProductTypes() + private function getProductTypes(): array { - if ($this->productTypes === null) { - $this->productTypes = []; - $block = $this->getLayout()->getBlock('category.products.list'); - if ($block) { - $productCollection = $block->getLoadedProductCollection(); - $productTypes = []; - /** @var $product \Magento\Catalog\Model\Product */ - foreach ($productCollection as $product) { - $productTypes[] = $this->escapeHtml($product->getTypeId()); - } - $this->productTypes = array_unique($productTypes); - } + if (count($this->productTypes) === 0) { + /** @var ProductTypeInterface productTypes */ + $this->productTypes = array_map(function ($productType) { + return $productType->getName(); + }, $this->productTypeList->getProductTypes()); } return $this->productTypes; } - - /** - * {@inheritdoc} - * @since 100.1.0 - */ - protected function _toHtml() - { - if (!$this->getProductTypes()) { - return ''; - } - return parent::_toHtml(); - } } diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..baa4bfcab4ebc --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerAddProductToWishlistCategoryPageActionGroup"> + <annotations> + <description>Adds the provided Product to the Wish List from the Storefront Category page. Validates that the Success Message is present and correct.</description> + </annotations> + <arguments> + <argument name="productVar"/> + </arguments> + + <click selector="{{StorefrontCategoryPageProductInfoSection.productAddToWishlist(productVar.id)}}" stepKey="addProductToWishlistClickAddToWishlist"/> + <waitForElement selector="{{StorefrontCustomerWishlistSection.successMsg}}" time="30" stepKey="addProductToWishlistWaitForSuccessMessage"/> + <see selector="{{StorefrontCustomerWishlistSection.successMsg}}" userInput="{{productVar.name}} has been added to your Wish List. Click here to continue shopping." stepKey="addProductToWishlistSeeProductNameAddedToWishlist"/> + <seeCurrentUrlMatches regex="~/wishlist_id/\d+/$~" stepKey="seeCurrentUrlMatches"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml new file mode 100644 index 0000000000000..638c8f4986a77 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckOptionsConfigurableProductInWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Move first Configurable Product with selected optional from Category Page to Wishlist."/> + <description value="Move first Configurable Product with selected optional from Category Page to Wishlist. On Page will be present minimum two Configurable Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14211"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createFirstConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createSecondConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteFirstProducts"> + <argument name="sku" value="$$createFirstConfigProduct.sku$$"/> + </actionGroup> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteSecondProducts"> + <argument name="sku" value="$$createSecondConfigProduct.sku$$"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute" > + <argument name="productAttributeLabel" value="{{visualSwatchAttribute.default_label}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToFirstConfigProductPage"> + <argument name="productId" value="$$createFirstConfigProduct.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> + <actionGroup ref="AddVisualSwatchToProductWithStorefrontConfigActionGroup" stepKey="addSwatchToFirstProduct"> + <argument name="attribute" value="visualSwatchAttribute"/> + <argument name="option1" value="visualSwatchOption1"/> + <argument name="option2" value="visualSwatchOption2"/> + </actionGroup> + + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToSecondConfigProductPage"> + <argument name="productId" value="$$createSecondConfigProduct.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <actionGroup ref="AddVisualSwatchToProductWithOutCreatedActionGroup" stepKey="addSwatchToSecondProduct"> + <argument name="attribute" value="visualSwatchAttribute"/> + </actionGroup> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup" stepKey="selectVisualSwatch"> + <argument name="productId" value="$$createFirstConfigProduct.id$$" /> + <argument name="visualSwatchOptionLabel" value="{{visualSwatchOption1.default_label}}" /> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistCategoryPageActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$$createFirstConfigProduct$$"/> + </actionGroup> + + <seeElement selector="{{StorefrontCustomerWishlistProductSection.productSeeDetailsByName($$createFirstConfigProduct.name$$)}}" stepKey="seeDetails"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml index a4860ace166d8..81bd966b904d7 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml @@ -21,7 +21,9 @@ template="Magento_Wishlist::catalog/product/list/addto/wishlist.phtml"/> </referenceBlock> <referenceContainer name="category.product.list.additional"> - <block class="Magento\Wishlist\Block\AddToWishlist" name="category.product.list.additional.wishlist_addto" template="Magento_Wishlist::addto.phtml" /> + <block class="Magento\Wishlist\Block\AddToWishlist" + name="category.product.list.additional.wishlist_addto" + template="Magento_Wishlist::addto.phtml"/> </referenceContainer> </referenceContainer> </body> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml index c293175ccceac..b26aa64ad89b1 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml @@ -14,5 +14,10 @@ template="Magento_Wishlist::catalog/product/list/addto/wishlist.phtml"/> </referenceBlock> </referenceContainer> + <referenceBlock name="wishlist_page_head_components"> + <block class="Magento\Wishlist\Block\AddToWishlist" + name="catalogsearch.wishlist_addto" + template="Magento_Wishlist::addto.phtml"/> + </referenceBlock> </body> </page> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 55cd77b196be5..62756f7211cee 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -17,7 +17,9 @@ define([ downloadableInfo: '#downloadable-links-list input', customOptionsInfo: '.product-custom-option', qtyInfo: '#qty', - actionElement: '[data-action="add-to-wishlist"]' + actionElement: '[data-action="add-to-wishlist"]', + productListWrapper: '.product-item-info', + productPageWrapper: '.product-info-main' }, /** @inheritdoc */ @@ -65,15 +67,19 @@ define([ _updateWishlistData: function (event) { var dataToAdd = {}, isFileUploaded = false, + handleObjSelector = null, self = this; if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq - this._updateAddToWishlistButton({}); + this._updateAddToWishlistButton({}, event); event.stopPropagation(); return; } - $(event.handleObj.selector).each(function (index, element) { + + handleObjSelector = $(event.currentTarget).closest('form').find(event.handleObj.selector); + + handleObjSelector.each(function (index, element) { if ($(element).is('input[type=text]') || $(element).is('input[type=email]') || $(element).is('input[type=number]') || @@ -98,18 +104,20 @@ define([ if (isFileUploaded) { this.bindFormSubmit(); } - this._updateAddToWishlistButton(dataToAdd); + this._updateAddToWishlistButton(dataToAdd, event); event.stopPropagation(); }, /** * @param {Object} dataToAdd + * @param {jQuery.Event} event * @private */ - _updateAddToWishlistButton: function (dataToAdd) { - var self = this; + _updateAddToWishlistButton: function (dataToAdd, event) { + var self = this, + buttons = this._getAddToWishlistButton(event); - $('[data-action="add-to-wishlist"]').each(function (index, element) { + buttons.each(function (index, element) { var params = $(element).data('post'); if (!params) { @@ -125,6 +133,20 @@ define([ }); }, + /** + * @param {jQuery.Event} event + * @private + */ + _getAddToWishlistButton: function (event) { + var productListWrapper = $(event.currentTarget).closest(this.options.productListWrapper); + + if (productListWrapper.length) { + return productListWrapper.find(this.options.actionElement); + } + + return $(event.currentTarget).closest(this.options.productPageWrapper).find(this.options.actionElement); + }, + /** * @param {Object} array1 * @param {Object} array2 diff --git a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less index 07317e1670a0b..ce1b009c24d42 100644 --- a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less @@ -65,7 +65,6 @@ // _____________________________________________ & when (@media-common = true) { - .swatch { &-attribute { &-label { @@ -155,7 +154,7 @@ padding: 4px 8px; &.selected { - .lib-css(background-color, @swatch-option-text__selected__background-color) !important; + .lib-css(background-color, @swatch-option-text__selected__background-color); } } @@ -201,6 +200,7 @@ top: 0; } } + &-disabled { border: 0; cursor: default; @@ -208,6 +208,7 @@ &:after { .lib-rotate(-30deg); + .lib-css(background, @swatch-option__disabled__background); content: ''; height: 2px; left: -4px; @@ -215,7 +216,6 @@ top: 10px; width: 42px; z-index: 995; - .lib-css(background, @swatch-option__disabled__background); } } @@ -226,6 +226,7 @@ &-tooltip { .lib-css(border, @swatch-option-tooltip__border); .lib-css(color, @swatch-option-tooltip__color); + .lib-css(background, @swatch-option-tooltip__background); display: none; max-height: 100%; min-height: 20px; @@ -234,7 +235,6 @@ position: absolute; text-align: center; z-index: 999; - .lib-css(background, @swatch-option-tooltip__background); &, &-layered { @@ -278,9 +278,9 @@ } &-layered { + .lib-css(background, @swatch-option-tooltip-layered__background); .lib-css(border, @swatch-option-tooltip-layered__border); .lib-css(color, @swatch-option-tooltip-layered__color); - .lib-css(background, @swatch-option-tooltip-layered__background); display: none; left: -47px; position: absolute; @@ -326,7 +326,6 @@ margin: 2px 0; padding: 2px; position: static; - z-index: 1; } &-visual-tooltip-layered { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 5d9746317af55..2c8c52bdb7af2 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -267,7 +267,7 @@ .lib-icon-font-symbol( @_icon-font-content: @icon-trash ); - + &:hover { .lib-css(text-decoration, @link__text-decoration); } @@ -574,7 +574,7 @@ .widget { float: left; - + &.block { margin-bottom: @indent__base; } @@ -727,9 +727,14 @@ position: static; } } + &.discount { width: auto; } + + .actions-toolbar { + width: auto; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less index 494483ff60dda..8fbe67abe2960 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -49,6 +49,10 @@ form { &.form-purchase-order { margin-bottom: 15px; + + .input-text { + width: 40%; + } } } } @@ -119,7 +123,7 @@ margin: 0 0 @indent__base; .primary { - .action-update { + .action-update { margin-bottom: 20px; margin-right: 0; } @@ -133,7 +137,7 @@ .lib-css(line-height, @checkout-billing-address-details__line-height); .lib-css(padding, @checkout-billing-address-details__padding); } - + input[type="checkbox"] { vertical-align: top; } diff --git a/app/etc/di.xml b/app/etc/di.xml index 585c88f68ff6f..e2b416b4f0fcf 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1833,6 +1833,61 @@ </argument> </arguments> </type> + <virtualType name="DefaultWYSIWYGValidator" type="Magento\Framework\Validator\HTML\ConfigurableWYSIWYGValidator"> + <arguments> + <argument name="allowedTags" xsi:type="array"> + <item name="div" xsi:type="string">div</item> + <item name="a" xsi:type="string">a</item> + <item name="p" xsi:type="string">p</item> + <item name="span" xsi:type="string">span</item> + <item name="em" xsi:type="string">em</item> + <item name="strong" xsi:type="string">strong</item> + <item name="ul" xsi:type="string">ul</item> + <item name="li" xsi:type="string">li</item> + <item name="ol" xsi:type="string">ol</item> + <item name="h5" xsi:type="string">h5</item> + <item name="h4" xsi:type="string">h4</item> + <item name="h3" xsi:type="string">h3</item> + <item name="h2" xsi:type="string">h2</item> + <item name="h1" xsi:type="string">h1</item> + <item name="table" xsi:type="string">table</item> + <item name="tbody" xsi:type="string">tbody</item> + <item name="tr" xsi:type="string">tr</item> + <item name="td" xsi:type="string">td</item> + <item name="th" xsi:type="string">th</item> + <item name="tfoot" xsi:type="string">tfoot</item> + <item name="img" xsi:type="string">img</item> + <item name="hr" xsi:type="string">hr</item> + <item name="figure" xsi:type="string">figure</item> + <item name="button" xsi:type="string">button</item> + </argument> + <argument name="allowedAttributes" xsi:type="array"> + <item name="class" xsi:type="string">class</item> + <item name="width" xsi:type="string">width</item> + <item name="height" xsi:type="string">height</item> + <item name="style" xsi:type="string">style</item> + <item name="alt" xsi:type="string">alt</item> + <item name="title" xsi:type="string">title</item> + <item name="border" xsi:type="string">border</item> + <item name="id" xsi:type="string">id</item> + </argument> + <argument name="attributesAllowedByTags" xsi:type="array"> + <item name="a" xsi:type="array"> + <item name="href" xsi:type="string">href</item> + </item> + <item name="img" xsi:type="array"> + <item name="src" xsi:type="string">src</item> + </item> + <item name="button" xsi:type="array"> + <item name="type" xsi:type="string">type</item> + </item> + </argument> + <argument name="attributeValidators" xsi:type="array"> + <item name="style" xsi:type="object">Magento\Framework\Validator\HTML\StyleAttributeValidator</item> + </argument> + </arguments> + </virtualType> + <preference for="Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface" type="DefaultWYSIWYGValidator" /> <type name="Magento\Framework\View\TemplateEngine\Php"> <arguments> <argument name="blockVariables" xsi:type="array"> diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php index fd0519ab2b34e..5623edca62b9a 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -4,16 +4,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Api; use Magento\Authorization\Model\Role; use Magento\Authorization\Model\RoleFactory; use Magento\Authorization\Model\Rules; use Magento\Authorization\Model\RulesFactory; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; +use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Integration\Api\AdminTokenServiceInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\UrlRewrite\Model\Storage\DbStorage; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** @@ -145,14 +151,14 @@ public function testCreate() */ public function testDelete() { - /** @var \Magento\UrlRewrite\Model\Storage\DbStorage $storage */ - $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + /** @var DbStorage $storage */ + $storage = Bootstrap::getObjectManager()->get(DbStorage::class); $categoryId = $this->modelId; $data = [ UrlRewrite::ENTITY_ID => $categoryId, UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE ]; - /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $urlRewrite*/ + /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $urlRewrite */ $urlRewrite = $storage->findOneByData($data); // Assert that a url rewrite is auto-generated for the category created from the data fixture @@ -189,7 +195,7 @@ public function testDeleteSystemOrRoot() public function deleteSystemOrRootDataProvider() { return [ - [\Magento\Catalog\Model\Category::TREE_ROOT_ID], + [Category::TREE_ROOT_ID], [2] //Default root category ]; } @@ -212,8 +218,8 @@ public function testUpdate() ]; $result = $this->updateCategory($categoryId, $categoryData); $this->assertEquals($categoryId, $result['id']); - /** @var \Magento\Catalog\Model\Category $model */ - $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); $category = $model->load($categoryId); $this->assertFalse((bool)$category->getIsActive(), 'Category "is_active" must equal to false'); $this->assertEquals("Update Category Test", $category->getName()); @@ -240,8 +246,8 @@ public function testUpdateWithDefaultSortByAttribute() ]; $result = $this->updateCategory($categoryId, $categoryData); $this->assertEquals($categoryId, $result['id']); - /** @var \Magento\Catalog\Model\Category $model */ - $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); $category = $model->load($categoryId); $this->assertTrue((bool)$category->getIsActive(), 'Category "is_active" must equal to true'); $this->assertEquals("Update Category Test With default_sort_by Attribute", $category->getName()); @@ -284,13 +290,13 @@ public function testUpdateUrlKey() ]; $result = $this->updateCategory($categoryId, $categoryData); $this->assertEquals($categoryId, $result['id']); - /** @var \Magento\Catalog\Model\Category $model */ - $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); $category = $model->load($categoryId); $this->assertEquals("Update Category Test New Name", $category->getName()); // check for the url rewrite for the new name - $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $storage = Bootstrap::getObjectManager()->get(DbStorage::class); $data = [ UrlRewrite::ENTITY_ID => $categoryId, UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, @@ -307,7 +313,7 @@ public function testUpdateUrlKey() $this->assertEquals('update-category-test-new-name.html', $urlRewrite->getRequestPath()); // check for the forward from the old name to the new name - $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $storage = Bootstrap::getObjectManager()->get(DbStorage::class); $data = [ UrlRewrite::ENTITY_ID => $categoryId, UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, @@ -427,14 +433,9 @@ protected function updateCategory($id, $data, ?string $token = null) if ($token) { $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; } + $data['id'] = $id; - if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { - $data['id'] = $id; - return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); - } else { - $data['id'] = $id; - return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); - } + return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); } /** @@ -557,6 +558,82 @@ public function testSaveDesign(): void $this->createdCategories = [$result['id']]; } + /** + * Check if repository does not override default values for attributes out of request + * + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testUpdateScopeAttribute() + { + $categoryId = 333; + $categoryData = [ + 'name' => 'Scope Specific Value', + ]; + $result = $this->updateCategoryForSpecificStore($categoryId, $categoryData); + $this->assertEquals($categoryId, $result['id']); + + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); + $category = $model->load($categoryId); + + /** @var ScopeOverriddenValue $scopeOverriddenValue */ + $scopeOverriddenValue = Bootstrap::getObjectManager()->get(ScopeOverriddenValue::class); + self::assertTrue($scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + 'name', + Store::DISTRO_STORE_ID + ), 'Name is not saved for specific store'); + self::assertFalse($scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + 'is_active', + Store::DISTRO_STORE_ID + ), 'is_active is overridden for default store'); + self::assertFalse($scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + 'url_key', + Store::DISTRO_STORE_ID + ), 'url_key is overridden for default store'); + + $this->deleteCategory($categoryId); + } + + /** + * Update given category via web API for specific store code. + * + * @param int $id + * @param array $data + * @param string|null $token + * @param string $storeCode + * @return array + */ + protected function updateCategoryForSpecificStore( + int $id, + array $data, + ?string $token = null, + string $storeCode = 'default' + ) { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $id, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + if ($token) { + $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; + } + $data['id'] = $id; + + return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data], null, $storeCode); + } + /** * @inheritDoc * diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php new file mode 100644 index 0000000000000..fd815c6d2241b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php @@ -0,0 +1,284 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Api; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\Website\Link; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Tests for products creation for all store views. + * + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductRepositoryAllStoreViewsTest extends WebapiAbstract +{ + const PRODUCT_SERVICE_NAME = 'catalogProductRepositoryV1'; + const SERVICE_VERSION = 'V1'; + const PRODUCTS_RESOURCE_PATH = '/V1/products'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var Registry + */ + private $registry; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Link + */ + private $productWebsiteLink; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @var string + */ + private $productSku = 'simple'; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->eavConfig = $this->objectManager->get(Config::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->productWebsiteLink = $this->objectManager->get(Link::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + try { + $this->productRepository->deleteById($this->productSku); + } catch (NoSuchEntityException $e) { + //already deleted + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + * @return void + */ + public function testCreateProduct(): void + { + $productData = $this->getProductData(); + $resultData = $this->saveProduct($productData); + $this->assertProductWebsites($this->productSku, $this->getAllWebsiteIds()); + $this->assertProductData($productData, $resultData, $this->getAllWebsiteIds()); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @return void + */ + public function testCreateProductOnMultipleWebsites(): void + { + $productData = $this->getProductData(); + $resultData = $this->saveProduct($productData); + $this->assertProductWebsites($this->productSku, $this->getAllWebsiteIds()); + $this->assertProductData($productData, $resultData, $this->getAllWebsiteIds()); + } + + /** + * Saves product via API. + * + * @param array $product + * @return array + */ + private function saveProduct(array $product): array + { + $serviceInfo = [ + 'rest' => ['resourcePath' =>self::PRODUCTS_RESOURCE_PATH, 'httpMethod' => Request::HTTP_METHOD_POST], + 'soap' => [ + 'service' => self::PRODUCT_SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::PRODUCT_SERVICE_NAME . 'Save' + ] + ]; + $requestData = ['product' => $product]; + return $this->_webApiCall($serviceInfo, $requestData, null, 'all'); + } + + /** + * Returns product data. + * + * @return array + */ + private function getProductData(): array + { + $setId = (int)$this->eavConfig->getEntityType(ProductAttributeInterface::ENTITY_TYPE_CODE) + ->getDefaultAttributeSetId(); + + return [ + ProductInterface::SKU => $this->productSku, + ProductInterface::NAME => 'simple', + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::WEIGHT => 1, + ProductInterface::ATTRIBUTE_SET_ID => $setId, + ProductInterface::PRICE => 10, + ProductInterface::STATUS => Status::STATUS_ENABLED, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::EXTENSION_ATTRIBUTES_KEY => [ + 'stock_item' => [ + StockItemInterface::IS_IN_STOCK => 1, + StockItemInterface::QTY => 1000, + StockItemInterface::IS_QTY_DECIMAL => 0, + StockItemInterface::SHOW_DEFAULT_NOTIFICATION_MESSAGE => 0, + StockItemInterface::USE_CONFIG_MIN_QTY => 0, + StockItemInterface::USE_CONFIG_MIN_SALE_QTY => 0, + StockItemInterface::MIN_QTY => 1, + StockItemInterface::MIN_SALE_QTY => 1, + StockItemInterface::MAX_SALE_QTY => 100, + StockItemInterface::USE_CONFIG_MAX_SALE_QTY => 0, + StockItemInterface::USE_CONFIG_BACKORDERS => 0, + StockItemInterface::BACKORDERS => 0, + StockItemInterface::USE_CONFIG_NOTIFY_STOCK_QTY => 0, + StockItemInterface::NOTIFY_STOCK_QTY => 0, + StockItemInterface::USE_CONFIG_QTY_INCREMENTS => 0, + StockItemInterface::QTY_INCREMENTS => 0, + StockItemInterface::USE_CONFIG_ENABLE_QTY_INC => 0, + StockItemInterface::ENABLE_QTY_INCREMENTS => 0, + StockItemInterface::USE_CONFIG_MANAGE_STOCK => 1, + StockItemInterface::MANAGE_STOCK => 1, + StockItemInterface::LOW_STOCK_DATE => null, + StockItemInterface::IS_DECIMAL_DIVIDED => 0, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => 0, + ], + ], + ProductInterface::CUSTOM_ATTRIBUTES => [ + ['attribute_code' => 'url_key', 'value' => 'simple'], + ['attribute_code' => 'tax_class_id', 'value' => 2], + ['attribute_code' => 'category_ids', 'value' => [333]] + ] + ]; + } + + /** + * Asserts that product is linked to websites in 'catalog_product_website' table. + * + * @param string $sku + * @param array $websiteIds + * @return void + */ + private function assertProductWebsites(string $sku, array $websiteIds): void + { + $productId = $this->productRepository->get($sku)->getId(); + $this->assertEquals($websiteIds, $this->productWebsiteLink->getWebsiteIdsByProductId($productId)); + } + + /** + * Asserts result after product creation. + * + * @param array $productData + * @param array $resultData + * @param array $websiteIds + * @return void + */ + private function assertProductData(array $productData, array $resultData, array $websiteIds): void + { + foreach ($productData as $key => $value) { + if ($key == 'extension_attributes' || $key == 'custom_attributes') { + continue; + } + $this->assertEquals($value, $resultData[$key]); + } + foreach ($productData['custom_attributes'] as $attribute) { + $resultAttribute = $this->getCustomAttributeByCode( + $resultData['custom_attributes'], + $attribute['attribute_code'] + ); + if ($attribute['attribute_code'] == 'category_ids') { + $this->assertEquals(array_values($attribute['value']), array_values($resultAttribute['value'])); + continue; + } + $this->assertEquals($attribute['value'], $resultAttribute['value']); + } + foreach ($productData['extension_attributes']['stock_item'] as $key => $value) { + $this->assertEquals($value, $resultData['extension_attributes']['stock_item'][$key]); + } + $this->assertEquals($websiteIds, $resultData['extension_attributes']['website_ids']); + } + + /** + * Get list of all websites IDs. + * + * @return array + */ + private function getAllWebsiteIds(): array + { + $websiteIds = []; + foreach ($this->storeManager->getWebsites() as $website) { + $websiteIds[] = $website->getId(); + } + + return $websiteIds; + } + + /** + * Returns custom attribute data by given code. + * + * @param array $attributes + * @param string $attributeCode + * @return array + */ + private function getCustomAttributeByCode(array $attributes, string $attributeCode): array + { + $items = array_filter( + $attributes, + function ($attribute) use ($attributeCode) { + return $attribute['attribute_code'] == $attributeCode; + } + ); + + return reset($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php new file mode 100644 index 0000000000000..8ff4e29b46dde --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for view category block. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var GetCategoryByName */ + private $getCategoryByName; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var LayoutInterface */ + private $layout; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->getCategoryByName = $this->objectManager->get(GetCategoryByName::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_cms_block.php + * + * @return void + */ + public function testCmsBlockDisplayedOnCategory(): void + { + $storeId = (int)$this->storeManager->getStore('default')->getId(); + $categoryId = $this->getCategoryByName->execute('Category with cms block')->getId(); + $category = $this->categoryRepository->get($categoryId, $storeId); + $this->registerCategory($category); + $block = $this->layout->createBlock(View::class)->setTemplate('Magento_Catalog::category/cms.phtml'); + $this->assertStringContainsString('<h1>Fixture Block Title</h1>', $block->toHtml()); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('current_category'); + $this->registry->register('current_category', $category); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php new file mode 100644 index 0000000000000..dc74a2c2cba7b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category\Save; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Test cases for save category controller. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class SaveCategoryTest extends AbstractSaveCategoryTest +{ + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var GetBlockByIdentifierInterface */ + private $getBlockByIdentifier; + + /** @var string */ + private $createdCategoryId; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->getBlockByIdentifier = $this->_objectManager->get(GetBlockByIdentifierInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if (!empty($this->createdCategoryId)) { + try { + $this->categoryRepository->deleteByIdentifier($this->createdCategoryId); + } catch (NoSuchEntityException $e) { + //Category already deleted. + } + $this->createdCategoryId = null; + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Cms/_files/block.php + * + * @return void + */ + public function testCreateCategoryWithCmsBlock(): void + { + $storeId = (int)$this->storeManager->getStore('default')->getId(); + $blockId = $this->getBlockByIdentifier->execute('fixture_block', $storeId)->getId(); + $postData = [ + CategoryInterface::KEY_NAME => 'Category with cms block', + CategoryInterface::KEY_IS_ACTIVE => 1, + CategoryInterface::KEY_INCLUDE_IN_MENU => 1, + 'display_mode' => Category::DM_MIXED, + 'landing_page' => $blockId, + CategoryInterface::KEY_AVAILABLE_SORT_BY => ['position'], + 'default_sort_by' => 'position', + ]; + $responseData = $this->performSaveCategoryRequest($postData); + $this->assertRequestIsSuccessfullyPerformed($responseData); + $this->createdCategoryId = $responseData['category']['entity_id']; + $category = $this->categoryRepository->get($this->createdCategoryId); + $this->assertEquals($blockId, $category->getLandingPage()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php new file mode 100644 index 0000000000000..75b96a1af3b09 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category\Save; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Test related to update category. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class UpdateCategoryTest extends AbstractSaveCategoryTest +{ + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + } + + /** + * @dataProvider categoryDataProvider + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * + * @param array $postData + * @return void + */ + public function testUpdateCategoryForDefaultStoreView(array $postData): void + { + $storeId = (int)$this->storeManager->getStore('default')->getId(); + $postData = array_merge($postData, ['store_id' => $storeId]); + $responseData = $this->performSaveCategoryRequest($postData); + $this->assertRequestIsSuccessfullyPerformed($responseData); + $category = $this->categoryRepository->get($postData['entity_id'], $postData['store_id']); + unset($postData['use_default']); + unset($postData['use_config']); + foreach ($postData as $key => $value) { + $this->assertEquals($value, $category->getData($key)); + } + } + + /** + * @return array + */ + public function categoryDataProvider(): array + { + return [ + [ + 'post_data' => [ + 'entity_id' => 333, + CategoryInterface::KEY_IS_ACTIVE => '0', + CategoryInterface::KEY_INCLUDE_IN_MENU => '0', + CategoryInterface::KEY_NAME => 'Category default store', + 'description' => 'Description for default store', + 'landing_page' => '', + 'display_mode' => Category::DM_MIXED, + CategoryInterface::KEY_AVAILABLE_SORT_BY => ['name', 'price'], + 'default_sort_by' => 'price', + 'filter_price_range' => 5, + 'url_key' => 'default-store-category', + 'meta_title' => 'meta_title default store', + 'meta_keywords' => 'meta_keywords default store', + 'meta_description' => 'meta_description default store', + 'custom_use_parent_settings' => '0', + 'custom_design' => '2', + 'page_layout' => '2columns-right', + 'custom_apply_to_products' => '1', + 'use_default' => [ + CategoryInterface::KEY_NAME => '0', + CategoryInterface::KEY_IS_ACTIVE => '0', + CategoryInterface::KEY_INCLUDE_IN_MENU => '0', + 'url_key' => '0', + 'meta_title' => '0', + 'custom_use_parent_settings' => '0', + 'custom_apply_to_products' => '0', + 'description' => '0', + 'landing_page' => '0', + 'display_mode' => '0', + 'custom_design' => '0', + 'page_layout' => '0', + 'meta_keywords' => '0', + 'meta_description' => '0', + 'custom_layout_update' => '0', + ], + 'use_config' => [ + CategoryInterface::KEY_AVAILABLE_SORT_BY => false, + 'default_sort_by' => false, + 'filter_price_range' => false, + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 6245e4e9f8de7..cd58cd2ac3819 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -10,6 +10,8 @@ use Magento\Framework\Acl\Builder; use Magento\Backend\App\Area\FrontNameResolver; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ProductMetadata; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\Framework\Registry; @@ -270,7 +272,7 @@ public function testSuggestCategoriesActionNoSuggestions(): void */ public function saveActionDataProvider(): array { - return [ + $result = [ 'default values' => [ [ 'id' => '2', @@ -390,6 +392,20 @@ public function saveActionDataProvider(): array ], ], ]; + + $productMetadataInterface = Bootstrap::getObjectManager()->get(ProductMetadataInterface::class); + if ($productMetadataInterface->getEdition() !== ProductMetadata::EDITION_NAME) { + /** + * Skip save custom_design_from and custom_design_to attributes, + * because this logic is rewritten on EE by Catalog Schedule + */ + foreach (array_keys($result['custom values']) as $index) { + unset($result['custom values'][$index]['custom_design_from']); + unset($result['custom values'][$index]['custom_design_to']); + } + } + + return $result; } /** @@ -398,6 +414,11 @@ public function saveActionDataProvider(): array */ public function testIncorrectDateFrom(): void { + $productMetadataInterface = Bootstrap::getObjectManager()->get(ProductMetadataInterface::class); + if ($productMetadataInterface->getEdition() !== ProductMetadata::EDITION_NAME) { + $this->markTestSkipped('Skipped, because this logic is rewritten on EE by Catalog Schedule'); + } + $data = [ 'name' => 'Test Category', 'attribute_set_id' => '3', diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php index 7fd7627c738d6..e829801d60e1a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -8,93 +8,83 @@ namespace Magento\Catalog\Model; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\CategoryRepositoryInterfaceFactory; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Cms\Api\GetBlockByIdentifierInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\StoreManagementInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** * Provide tests for CategoryRepository model. + * + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryRepositoryTest extends TestCase { - private const FIXTURE_CATEGORY_ID = 333; - private const FIXTURE_TWO_STORES_CATEGORY_ID = 555; - private const FIXTURE_SECOND_STORE_CODE = 'fixturestore'; - private const FIXTURE_FIRST_STORE_CODE = 'default'; + /** @var ObjectManagerInterface */ + private $objectManager; - /** - * @var CategoryLayoutUpdateManager - */ + /** @var CategoryLayoutUpdateManager */ private $layoutManager; - /** - * @var CategoryRepositoryInterfaceFactory - */ - private $repositoryFactory; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; - /** - * @var CollectionFactory - */ + /** @var CollectionFactory */ private $productCollectionFactory; - /** - * @var CategoryCollectionFactory - */ + /** @var CategoryCollectionFactory */ private $categoryCollectionFactory; + /** @var StoreManagementInterface */ + private $storeManager; + + /** @var GetBlockByIdentifierInterface */ + private $getBlockByIdentifier; + /** - * Sets up common objects. - * - * @inheritDoc + * @inheritdoc */ protected function setUp(): void { - Bootstrap::getObjectManager()->configure([ + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->configure([ 'preferences' => [ \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class ] ]); - $this->repositoryFactory = Bootstrap::getObjectManager()->get(CategoryRepositoryInterfaceFactory::class); - $this->layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); - $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); - $this->categoryCollectionFactory = Bootstrap::getObjectManager()->create(CategoryCollectionFactory::class); - } - - /** - * Create subject object. - * - * @return CategoryRepositoryInterface - */ - private function createRepo(): CategoryRepositoryInterface - { - return $this->repositoryFactory->create(); + $this->layoutManager = $this->objectManager->get(CategoryLayoutUpdateManager::class); + $this->productCollectionFactory = $this->objectManager->get(CollectionFactory::class); + $this->categoryCollectionFactory = $this->objectManager->get(CategoryCollectionFactory::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->getBlockByIdentifier = $this->objectManager->get(GetBlockByIdentifierInterface::class); } /** * Test that custom layout file attribute is saved. * - * @return void - * @throws \Throwable * @magentoDataFixture Magento/Catalog/_files/category.php - * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * + * @return void */ public function testCustomLayout(): void { - //New valid value - $repo = $this->createRepo(); - $category = $repo->get(self::FIXTURE_CATEGORY_ID); + $category = $this->categoryRepository->get(333); $newFile = 'test'; - $this->layoutManager->setCategoryFakeFiles(self::FIXTURE_CATEGORY_ID, [$newFile]); + $this->layoutManager->setCategoryFakeFiles(333, [$newFile]); $category->setCustomAttribute('custom_layout_update_file', $newFile); - $repo->save($category); - $repo = $this->createRepo(); - $category = $repo->get(self::FIXTURE_CATEGORY_ID); + $this->categoryRepository->save($category); + $category = $this->categoryRepository->get(333); $this->assertEquals($newFile, $category->getCustomAttribute('custom_layout_update_file')->getValue()); //Setting non-existent value @@ -102,7 +92,7 @@ public function testCustomLayout(): void $category->setCustomAttribute('custom_layout_update_file', $newFile); $caughtException = false; try { - $repo->save($category); + $this->categoryRepository->save($category); } catch (LocalizedException $exception) { $caughtException = true; } @@ -112,9 +102,9 @@ public function testCustomLayout(): void /** * Test removal of categories. * - * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoAppArea adminhtml + * * @return void */ public function testCategoryBehaviourAfterDelete(): void @@ -122,7 +112,7 @@ public function testCategoryBehaviourAfterDelete(): void $productCollection = $this->productCollectionFactory->create(); $deletedCategories = ['3', '4', '5', '13']; $categoryCollectionIds = $this->categoryCollectionFactory->create()->getAllIds(); - $this->createRepo()->deleteByIdentifier(3); + $this->categoryRepository->deleteByIdentifier(3); $this->assertEquals( 0, $productCollection->addCategoriesFilter(['in' => $deletedCategories])->getSize(), @@ -131,42 +121,87 @@ public function testCategoryBehaviourAfterDelete(): void $newCategoryCollectionIds = $this->categoryCollectionFactory->create()->getAllIds(); $difference = array_diff($categoryCollectionIds, $newCategoryCollectionIds); sort($difference); - $this->assertEquals( - $deletedCategories, - $difference, - 'Wrong categories was deleted' - ); + $this->assertEquals($deletedCategories, $difference, 'Wrong categories was deleted'); } /** * Verifies whether `get()` method `$storeId` attribute works as expected. * - * @magentoDbIsolation enabled * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDataFixture Magento/Catalog/_files/category_with_two_stores.php + * + * @return void */ - public function testGetCategoryForProvidedStore() + public function testGetCategoryForProvidedStore(): void { - $categoryRepository = $this->repositoryFactory->create(); - - $categoryDefault = $categoryRepository->get( - self::FIXTURE_TWO_STORES_CATEGORY_ID - ); - + $categoryId = 555; + $categoryDefault = $this->categoryRepository->get($categoryId); $this->assertSame('category-defaultstore', $categoryDefault->getUrlKey()); - - $categoryFirstStore = $categoryRepository->get( - self::FIXTURE_TWO_STORES_CATEGORY_ID, - self::FIXTURE_FIRST_STORE_CODE - ); - + $defaultStoreId = $this->storeManager->getStore('default')->getId(); + $categoryFirstStore = $this->categoryRepository->get($categoryId, $defaultStoreId); $this->assertSame('category-defaultstore', $categoryFirstStore->getUrlKey()); + $fixtureStoreId = $this->storeManager->getStore('fixturestore')->getId(); + $categorySecondStore = $this->categoryRepository->get($categoryId, $fixtureStoreId); + $this->assertSame('category-fixturestore', $categorySecondStore->getUrlKey()); + } - $categorySecondStore = $categoryRepository->get( - self::FIXTURE_TWO_STORES_CATEGORY_ID, - self::FIXTURE_SECOND_STORE_CODE - ); + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Cms/_files/block.php + * + * @return void + */ + public function testUpdateCategoryDefaultStoreView(): void + { + $categoryId = 333; + $defaultStoreId = (int)$this->storeManager->getStore('default')->getId(); + $secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId(); + $blockId = $this->getBlockByIdentifier->execute('fixture_block', $defaultStoreId)->getId(); + $origData = $this->categoryRepository->get($categoryId)->getData(); + unset($origData[CategoryInterface::KEY_UPDATED_AT]); + $category = $this->categoryRepository->get($categoryId, $defaultStoreId); + $dataForDefaultStore = [ + CategoryInterface::KEY_IS_ACTIVE => 0, + CategoryInterface::KEY_INCLUDE_IN_MENU => 0, + CategoryInterface::KEY_NAME => 'Category default store', + 'image' => 'test.png', + 'description' => 'Description for default store', + 'landing_page' => $blockId, + 'display_mode' => Category::DM_MIXED, + CategoryInterface::KEY_AVAILABLE_SORT_BY => ['name', 'price'], + 'default_sort_by' => 'price', + 'filter_price_range' => 5, + 'url_key' => 'default-store-category', + 'meta_title' => 'meta_title default store', + 'meta_keywords' => 'meta_keywords default store', + 'meta_description' => 'meta_description default store', + 'custom_use_parent_settings' => '0', + 'custom_design' => '2', + 'page_layout' => '2columns-right', + 'custom_apply_to_products' => '1', + ]; + $category->addData($dataForDefaultStore); + $updatedCategory = $this->categoryRepository->save($category); + $this->assertCategoryData($dataForDefaultStore, $updatedCategory); + $categorySecondStore = $this->categoryRepository->get($categoryId, $secondStoreId); + $this->assertCategoryData($origData, $categorySecondStore); + foreach ($dataForDefaultStore as $key => $value) { + $this->assertNotEquals($value, $categorySecondStore->getData($key)); + } + } - $this->assertSame('category-fixturestore', $categorySecondStore->getUrlKey()); + /** + * Assert category data. + * + * @param array $expectedData + * @param CategoryInterface $category + * @return void + */ + private function assertCategoryData(array $expectedData, CategoryInterface $category): void + { + foreach ($expectedData as $key => $value) { + $this->assertEquals($value, $category->getData($key)); + } } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 7ee2c62453df5..2d94466939dbe 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; @@ -15,6 +16,7 @@ use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\ObjectManagerInterface; @@ -84,11 +86,17 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase * @var StoreManagerInterface */ private $storeManager; + /** * @var int */ private $currentStoreId; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @inheritdoc */ @@ -108,6 +116,7 @@ protected function setUp(): void $this->mediaDirectory = $this->objectManager->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); $this->mediaDirectory->writeFile($this->fileName, 'Test'); + $this->metadataPool = $this->objectManager->get(MetadataPool::class); } /** @@ -351,6 +360,23 @@ public function testExecuteWithTwoImagesOnStoreView(): void } } + /** + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * + * @return void + */ + public function testDeleteSharedImage(): void + { + $product = $this->getProduct(null, 'simple'); + $this->duplicateMediaGalleryForProduct('/m/a/magento_image.jpg', 'simple2'); + $secondProduct = $this->getProduct(null, 'simple2'); + $this->updateHandler->execute($this->prepareRemoveImage($product), []); + $product = $this->getProduct(null, 'simple'); + $this->assertEmpty($product->getMediaGalleryImages()->getItems()); + $this->checkProductImageExist($secondProduct, '/m/a/magento_image.jpg'); + } + /** * @inheritdoc */ @@ -371,11 +397,13 @@ protected function tearDown(): void * Returns current product. * * @param int|null $storeId + * @param string|null $sku * @return ProductInterface|Product */ - private function getProduct(?int $storeId = null): ProductInterface + private function getProduct(?int $storeId = null, ?string $sku = null): ProductInterface { - return $this->productRepository->get('simple', false, $storeId, true); + $sku = $sku ?: 'simple'; + return $this->productRepository->get($sku, false, $storeId, true); } /** @@ -464,6 +492,86 @@ public function testDeleteWithMultiWebsites(): void $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); } + /** + * Check product image link and product image exist + * + * @param ProductInterface $product + * @param string $imagePath + * @return void + */ + private function checkProductImageExist(ProductInterface $product, string $imagePath): void + { + $productImageItem = $product->getMediaGalleryImages()->getFirstItem(); + $this->assertEquals($imagePath, $productImageItem->getFile()); + $productImageFile = $productImageItem->getPath(); + $this->assertNotEmpty($productImageFile); + $this->assertTrue($this->mediaDirectory->getDriver()->isExists($productImageFile)); + $this->fileName = $productImageFile; + } + + /** + * Prepare the product to remove image + * + * @param ProductInterface $product + * @return ProductInterface + */ + private function prepareRemoveImage(ProductInterface $product): ProductInterface + { + $item = $product->getMediaGalleryImages()->getFirstItem(); + $item->setRemoved('1'); + $galleryData = [ + 'images' => [ + (int)$item->getValueId() => $item->getData(), + ] + ]; + $product->setData(ProductInterface::MEDIA_GALLERY, $galleryData); + $product->setStoreId(0); + + return $product; + } + + /** + * Duplicate media gallery entries for a product + * + * @param string $imagePath + * @param string $productSku + * @return void + */ + private function duplicateMediaGalleryForProduct(string $imagePath, string $productSku): void + { + $product = $this->getProduct(null, $productSku); + $connect = $this->galleryResource->getConnection(); + $select = $connect->select()->from($this->galleryResource->getMainTable())->where('value = ?', $imagePath); + $result = $connect->fetchRow($select); + $value_id = $result['value_id']; + unset($result['value_id']); + $rows = [ + 'attribute_id' => $result['attribute_id'], + 'value' => $result['value'], + ProductAttributeMediaGalleryEntryInterface::MEDIA_TYPE => $result['media_type'], + ProductAttributeMediaGalleryEntryInterface::DISABLED => $result['disabled'], + ]; + $connect->insert($this->galleryResource->getMainTable(), $rows); + $select = $connect->select() + ->from($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE)) + ->where('value_id = ?', $value_id); + $result = $connect->fetchRow($select); + $newValueId = (int)$value_id + 1; + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + $rows = [ + 'value_id' => $newValueId, + 'store_id' => $result['store_id'], + ProductAttributeMediaGalleryEntryInterface::LABEL => $result['label'], + ProductAttributeMediaGalleryEntryInterface::POSITION => $result['position'], + ProductAttributeMediaGalleryEntryInterface::DISABLED => $result['disabled'], + $linkField => $product->getData($linkField), + ]; + $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE), $rows); + $rows = ['value_id' => $newValueId, $linkField => $product->getData($linkField)]; + $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE), $rows); + } + /** * @param Product $product * @param array $roles diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php new file mode 100644 index 0000000000000..417b791eb376a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Model\Category; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Cms/_files/block.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var CategoryInterfaceFactory $categoryFactory */ +$categoryFactory = $objectManager->get(CategoryInterfaceFactory::class); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var DefaultCategory $categoryHelper */ +$categoryHelper = $objectManager->get(DefaultCategory::class); +$currentStoreId = (int)$storeManager->getStore()->getId(); +/** @var GetBlockByIdentifierInterface $getBlockByIdentifierInterface */ +$getBlockByIdentifier = $objectManager->get(GetBlockByIdentifierInterface::class); +$block = $getBlockByIdentifier->execute('fixture_block', $currentStoreId); + +$category = $categoryFactory->create(); +$category->setName('Category with cms block') + ->setParentId($categoryHelper->getId()) + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setDisplayMode(Category::DM_MIXED) + ->setLandingPage($block->getId()); +try { + $storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $categoryRepository->save($category); +} finally { + $storeManager->setCurrentStore($currentStoreId); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_rollback.php new file mode 100644 index 0000000000000..4725fde47818c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$category = $getCategoryByName->execute('Category with cms block'); +if ($category->getId()) { + $categoryRepository->delete($category); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Cms/_files/block_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php new file mode 100644 index 0000000000000..35d4cceb50845 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product as ProductEntity; +use Magento\Catalog\Model\Product\Media\ConfigInterface; +use Magento\Framework\App\Bootstrap as AppBootstrap; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\ObjectManagerInterface; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Model\Import\Source\CsvFactory; +use Magento\ImportExport\Model\ResourceModel\Import\Data; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks that product import with same images can be successfully done + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ImportWithSharedImagesTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Filesystem */ + private $fileSystem; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var File */ + private $fileDriver; + + /** @var Import */ + private $import; + + /** @var ConfigInterface */ + private $mediaConfig; + + /** @var array */ + private $appParams; + + /** @var array */ + private $createdProductsSkus = []; + + /** @var array */ + private $filesToRemove = []; + + /** @var CsvFactory */ + private $csvFactory; + + /** @var Data */ + private $importDataResource; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->fileSystem = $this->objectManager->get(Filesystem::class); + $this->fileDriver = $this->objectManager->get(File::class); + $this->mediaConfig = $this->objectManager->get(ConfigInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->import = $this->objectManager->get(ProductFactory::class)->create(); + $this->csvFactory = $this->objectManager->get(CsvFactory::class); + $this->importDataResource = $this->objectManager->get(Data::class); + $this->appParams = Bootstrap::getInstance()->getBootstrap()->getApplication() + ->getInitParams()[AppBootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS]; + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->removeFiles(); + $this->removeProducts(); + $this->importDataResource->cleanBunches(); + + parent::tearDown(); + } + + /** + * @return void + */ + public function testImportProductsWithSameImages(): void + { + $this->moveImages('magento_image.jpg'); + $source = $this->prepareFile('catalog_import_products_with_same_images.csv'); + $this->updateUploader(); + $errors = $this->import->setParameters([ + 'behavior' => Import::BEHAVIOR_ADD_UPDATE, + 'entity' => ProductEntity::ENTITY, + ]) + ->setSource($source)->validateData(); + $this->assertEmpty($errors->getAllErrors()); + $this->import->importData(); + $this->createdProductsSkus = ['SimpleProductForTest1', 'SimpleProductForTest2']; + $this->checkProductsImages('/m/a/magento_image.jpg', $this->createdProductsSkus); + } + + /** + * Check product images + * + * @param string $expectedImagePath + * @param array $productSkus + * @return void + */ + private function checkProductsImages(string $expectedImagePath, array $productSkus): void + { + foreach ($productSkus as $productSku) { + $product = $this->productRepository->get($productSku); + $productImageItem = $product->getMediaGalleryImages()->getFirstItem(); + $productImageFile = $productImageItem->getFile(); + $productImagePath = $productImageItem->getPath(); + $this->filesToRemove[] = $productImagePath; + $this->assertEquals($expectedImagePath, $productImageFile); + $this->assertNotEmpty($productImagePath); + $this->assertTrue($this->fileDriver->isExists($productImagePath)); + } + } + + /** + * Remove created files + * + * @return void + */ + private function removeFiles(): void + { + foreach ($this->filesToRemove as $file) { + if ($this->fileDriver->isExists($file)) { + $this->fileDriver->deleteFile($file); + } + } + } + + /** + * Remove created products + * + * @return void + */ + private function removeProducts(): void + { + foreach ($this->createdProductsSkus as $sku) { + try { + $this->productRepository->deleteById($sku); + } catch (NoSuchEntityException $e) { + //already removed + } + } + } + + /** + * Prepare file + * + * @param string $fileName + * @return Csv + */ + private function prepareFile(string $fileName): Csv + { + $tmpDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $fixtureDir = realpath(__DIR__ . '/../../_files'); + $filePath = $tmpDirectory->getAbsolutePath($fileName); + $this->filesToRemove[] = $filePath; + $tmpDirectory->getDriver()->copy($fixtureDir . DIRECTORY_SEPARATOR . $fileName, $filePath); + $source = $this->csvFactory->create( + [ + 'file' => $fileName, + 'directory' => $tmpDirectory + ] + ); + + return $source; + } + + /** + * Update upload to use sandbox folders + * + * @return void + */ + private function updateUploader(): void + { + $uploader = $this->import->getUploader(); + $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); + $destDir = $rootDirectory->getRelativePath( + $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] + . DS . $this->mediaConfig->getBaseMediaPath() + ); + $tmpDir = $rootDirectory->getRelativePath( + $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] + ); + $rootDirectory->create($destDir); + $rootDirectory->create($tmpDir); + $uploader->setDestDir($destDir); + $uploader->setTmpDir($tmpDir); + } + + /** + * Move images to appropriate folder + * + * @param string $fileName + * @return void + */ + private function moveImages(string $fileName): void + { + $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); + $tmpDir = $rootDirectory->getRelativePath( + $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] + ); + $fixtureDir = realpath(__DIR__ . '/../../_files'); + $tmpFilePath = $rootDirectory->getAbsolutePath($tmpDir . DS . $fileName); + $this->fileDriver->createDirectory($tmpDir); + $rootDirectory->getDriver()->copy( + $fixtureDir . DIRECTORY_SEPARATOR . $fileName, + $tmpFilePath + ); + $this->filesToRemove[] = $tmpFilePath; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 4c4f2138abf35..669a9959de91c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -732,7 +732,7 @@ function ($input) { ) ); // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $option = array_merge(...$option); + $option = array_merge([], ...$option); if (!empty($option['type']) && !empty($option['name'])) { $lastOptionKey = $option['type'] . '|' . $option['name']; @@ -2275,7 +2275,7 @@ function (ProductInterface $item) { $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); $collection - ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge(...$categoryIds))]) + ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge([], ...$categoryIds))]) ->load() ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv new file mode 100644 index 0000000000000..7761ed7ac2360 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv @@ -0,0 +1,3 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +SimpleProductForTest1,,Default,simple,Default,base,SimpleProductAfterImport1,,,1,1,Taxable Goods,"Catalog, Search",250,,,,simple-product-for-test-1,,,,magento_image.jpg,BASE magento_image.jpg,magento_image.jpg,SMALL blueshirt,magento_image.jpg,Thumb Image,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,Block after Info Column,,,,,,,,,,,Use config,,,100,0,1,0,0,1,1,1,0,1,1,,1,0,1,1,0,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest2,,Default,simple,Default,base,SimpleProductAfterImport2,,,1,1,Taxable Goods,"Catalog, Search",300,,,,simple-product-for-test-2,,,,magento_image.jpg,BASE magento_image.jpg,magento_image.jpg,SMALL blueshirt,magento_image.jpg,Thumb Image,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,Block after Info Column,,,,,,,,,,,Use config,,,100,0,1,0,0,1,1,1,0,1,1,,1,0,1,1,0,1,0,0,0,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/magento_image.jpg b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/magento_image.jpg new file mode 100644 index 0000000000000..3b825a41b2101 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/magento_image.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php b/dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php new file mode 100644 index 0000000000000..cd9844dc98811 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Command; + +use Magento\Cms\Model\Wysiwyg\Validator; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Test the command. + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class WysiwygRestrictCommandTest extends TestCase +{ + /** + * @var ReinitableConfigInterface + */ + private $config; + + /** + * @var WysiwygRestrictCommandFactory + */ + private $factory; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->config = $objectManager->get(ReinitableConfigInterface::class); + $this->factory = $objectManager->get(WysiwygRestrictCommandFactory::class); + } + + /** + * "Execute" method cases. + * + * @return array + */ + public function getExecuteCases(): array + { + return [ + 'yes' => ['y', true], + 'no' => ['n', false], + 'no-but-different' => ['what', false] + ]; + } + + /** + * Test the command. + * + * @param string $argument + * @param bool $expectedFlag + * @return void + * @dataProvider getExecuteCases + * @magentoConfigFixture default_store cms/wysiwyg/force_valid 0 + */ + public function testExecute(string $argument, bool $expectedFlag): void + { + /** @var WysiwygRestrictCommand $model */ + $model = $this->factory->create(); + $tester = new CommandTester($model); + $tester->execute(['restrict' => $argument]); + + $this->config->reinit(); + $this->assertEquals($expectedFlag, $this->config->isSetFlag(Validator::CONFIG_PATH_THROW_EXCEPTION)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php index 0344d467a3cc2..214613821afb6 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php @@ -19,6 +19,8 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\LayoutInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -126,6 +128,29 @@ public function testGetAllowProducts(): void } } + /** + * Verify configurable option not assigned to current website won't be visible. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_two_websites.php + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * + * @return void + */ + public function testGetAllowProductsNonDefaultWebsite(): void + { + // Set current website to non-default. + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore('fixture_second_store'); + // Un-assign simple product from non-default website. + $simple = $this->productRepository->get('simple_Option_1'); + $simple->setWebsiteIds([1]); + $this->productRepository->save($simple); + // Verify only one configurable option will be visible. + $products = $this->block->getAllowProducts(); + $this->assertEquals(1, count($products)); + } + /** * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php new file mode 100644 index 0000000000000..351c84680389b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\AccountManagement; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for creation customer with address via customer account management service. + * + * @magentoDbIsolation enabled + */ +class CreateAccountWithAddressTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var CustomerInterfaceFactory */ + private $customerFactory; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var AddressInterfaceFactory */ + private $addressFactory; + + /** @var CustomerInterface */ + private $customer; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); + $this->customerFactory = $this->objectManager->get(CustomerInterfaceFactory::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->customer instanceof CustomerInterface) { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($this->customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoConfigFixture default_store general/country/allow BD,BB,AF + * @magentoConfigFixture fixture_second_store_store general/country/allow AS,BM + * @return void + */ + public function testCreateNewCustomerWithAddress(): void + { + $availableCountry = 'BD'; + $address = $this->addressFactory->create(); + $address->setCountryId($availableCountry) + ->setPostcode('75477') + ->setRegionId(1) + ->setStreet(['Green str, 67']) + ->setTelephone('3468676') + ->setCity('CityM') + ->setFirstname('John') + ->setLastname('Smith') + ->setIsDefaultShipping(true) + ->setIsDefaultBilling(true); + $customerEntity = $this->customerFactory->create(); + $customerEntity->setEmail('test@example.com') + ->setFirstname('John') + ->setLastname('Smith') + ->setStoreId(1); + $customerEntity->setAddresses([$address]); + $this->customer = $this->accountManagement->createAccount($customerEntity); + $this->assertCount(1, $this->customer->getAddresses(), 'The available address wasn\'t saved.'); + $this->assertSame( + $availableCountry, + $this->customer->getAddresses()[0]->getCountryId(), + 'The address was saved with disallowed country.' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php index eb638eeb329aa..79f8b1466d8d3 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php @@ -424,6 +424,23 @@ public function testAddressCreatedWithGroupAssignByVatIdWithError(): void $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); } + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoConfigFixture default_store general/country/allow BD,BB,AF + * @magentoConfigFixture fixture_second_store_store general/country/allow AS,BM + * + * @return void + */ + public function testCreateAvailableAddress(): void + { + $countryId = 'BB'; + $addressData = array_merge(self::STATIC_CUSTOMER_ADDRESS_DATA, [AddressInterface::COUNTRY_ID => $countryId]); + $customer = $this->customerRepository->get('customer5@example.com'); + $address = $this->createAddress((int)$customer->getId(), $addressData); + $this->assertSame($countryId, $address->getCountryId()); + } + /** * Create customer address with provided address data. * diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php index 7013346fd76e2..6098883959dd3 100644 --- a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php @@ -13,7 +13,7 @@ use Magento\SendFriend\Helper\Data as SendFriendHelper; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Zend\Stdlib\Parameters; +use Laminas\Stdlib\Parameters; /** * Class checks send friend model behavior @@ -28,6 +28,9 @@ class SendFriendTest extends TestCase /** @var SendFriend */ private $sendFriend; + /** @var ResourceModel\SendFriend */ + private $sendFriendResource; + /** @var CookieManagerInterface */ private $cookieManager; @@ -43,6 +46,7 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->sendFriend = $this->objectManager->get(SendFriendFactory::class)->create(); + $this->sendFriendResource = $this->objectManager->get(ResourceModel\SendFriend::class); $this->cookieManager = $this->objectManager->get(CookieManagerInterface::class); $this->request = $this->objectManager->get(RequestInterface::class); } @@ -55,6 +59,7 @@ protected function setUp(): void * @param array $sender * @param array $recipients * @param string|bool $expectedResult + * * @return void */ public function testValidate(array $sender, array $recipients, $expectedResult): void @@ -185,22 +190,34 @@ public function testisExceedLimitByCookies(): void * @magentoDataFixture Magento/SendFriend/_files/sendfriend_log_record_half_hour_before.php * * @magentoDbIsolation disabled + * * @return void */ public function testisExceedLimitByIp(): void { - $this->markTestSkipped('Blocked by MC-31968'); + $remoteAddr = '127.0.0.1'; $parameters = $this->objectManager->create(Parameters::class); - $parameters->set('REMOTE_ADDR', '127.0.0.1'); + $parameters->set('REMOTE_ADDR', $remoteAddr); $this->request->setServer($parameters); $this->assertTrue($this->sendFriend->isExceedLimit()); + // Verify that ip is saved correctly as integer value + $this->assertEquals( + 1, + (int)$this->sendFriendResource->getSendCount( + null, + ip2long($remoteAddr), + time() - (60 * 60 * 24 * 365), + 1 + ) + ); } /** - * Check result + * Check test result * * @param array|bool $expectedResult * @param array|bool $result + * * @return void */ private function checkResult($expectedResult, $result): void @@ -217,6 +234,7 @@ private function checkResult($expectedResult, $result): void * * @param array $sender * @param array $recipients + * * @return void */ private function prepareData(array $sender, array $recipients): void diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php new file mode 100644 index 0000000000000..fbbc6ef25cc09 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Shipping\Block\Adminhtml\Order; + +use Magento\Backend\Block\Template; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\ShipmentTrackInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * Class verifies packaging popup. + * + * @magentoAppArea adminhtml + */ +class AddToPackageTest extends TestCase +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + } + + /** + * Loads order entity by provided order increment ID. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrderByIncrementId(string $incrementId) : OrderInterface + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', $incrementId) + ->create(); + + $items = $this->orderRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Test that Packaging popup renders + * + * @magentoDataFixture Magento/Shipping/_files/shipping_with_carrier_data.php + */ + public function testGetCommentsHtml() + { + /** @var Template $block */ + $block = $this->objectManager->get(Packaging::class); + + $order = $this->getOrderByIncrementId('100000001'); + + /** @var ShipmentTrackInterface $track */ + $shipment = $order->getShipmentsCollection()->getFirstItem(); + + $this->registry->register('current_shipment', $shipment); + + $block->setTemplate('Magento_Shipping::order/packaging/popup.phtml'); + $html = $block->toHtml(); + $expectedNeedle = "packaging.setItemQtyCallback(function(itemId){ + var item = $$('[name=\"shipment[items]['+itemId+']\"]')[0], + itemTitle = $('order_item_' + itemId + '_title'); + if (!itemTitle && !item) { + return 0; + } + if (item && !isNaN(item.value)) { + return item.value; + } + });"; + $this->assertStringContainsString($expectedNeedle, $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php new file mode 100644 index 0000000000000..736487ac5c006 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Framework\DB\Transaction; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_with_customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->get(Transaction::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); +$order->setShippingDescription('UPS Next Day Air') + ->setShippingMethod('ups_11') + ->setShippingAmount(0) + ->setCouponCode('1234567890') + ->setDiscountDescription('1234567890'); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); + +$shipmentItems = []; +foreach ($order->getItems() as $orderItem) { + $shipmentItems[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} +$tracking = [ + 'carrier_code' => 'ups', + 'title' => 'United Parcel Service', + 'number' => '987654321' +]; + +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $shipmentItems, [$tracking]); +$shipment->register(); +$transaction->addObject($shipment)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data_rollback.php b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data_rollback.php new file mode 100644 index 0000000000000..bbb90e0326aec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_with_customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php index 98297cd43041f..b9ec091003267 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php @@ -52,7 +52,7 @@ function ($values, $index) use ($optionsPerAttribute) { ); $data['optionvisual']['value'] = array_reduce( range(1, $optionsPerAttribute), - function ($values, $index) use ($optionsPerAttribute) { + function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; return $values; }, @@ -61,7 +61,7 @@ function ($values, $index) use ($optionsPerAttribute) { $data['options']['option'] = array_reduce( range(1, $optionsPerAttribute), - function ($values, $index) use ($optionsPerAttribute) { + function ($values, $index) { $values[] = [ 'label' => 'option ' . $index, 'value' => 'option_' . $index diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php index ccf25fd15c529..06ba28932eeb5 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php @@ -40,7 +40,7 @@ public function testSwatchOptionAdd() $data['options']['option'] = array_reduce( range(10, $optionsPerAttribute), - function ($values, $index) use ($optionsPerAttribute) { + function ($values, $index) { $values[] = [ 'label' => 'option ' . $index, 'value' => 'option_' . $index diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php index 7493d1691699a..44d33362ae26e 100644 --- a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php @@ -16,8 +16,7 @@ class QueueInstallerTest extends TestCase { public function testInstall() { - $bindingInstaller = $this->getMockForAbstractClass(QueueConfigItemInterface::class); - $model = new QueueInstaller($bindingInstaller); + $model = new QueueInstaller(); $channel = $this->createMock(AMQPChannel::class); $queue = $this->getMockForAbstractClass(QueueConfigItemInterface::class); diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 2c3fbad4b9aaf..36993f1620e36 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -371,11 +371,11 @@ public function getMainConfigFiles($asDataSet = true) } $globPaths = [BP . '/app/etc/config.xml', BP . '/app/etc/*/config.xml']; $configXmlPaths = array_merge($globPaths, $configXmlPaths); - $files = [[]]; + $files = []; foreach ($configXmlPaths as $xmlPath) { $files[] = glob($xmlPath, GLOB_NOSORT); } - self::$_cache[$cacheKey] = array_merge(...$files); + self::$_cache[$cacheKey] = array_merge([], ...$files); } if ($asDataSet) { return self::composeDataSets(self::$_cache[$cacheKey]); diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php index dfe4b759e85be..c505c82789f81 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php @@ -120,7 +120,7 @@ function (string $ip) { public function getRemoteAddress(bool $ipToLong = false) { if ($this->remoteAddress !== null) { - return $this->remoteAddress; + return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } $remoteAddress = $this->readAddress(); @@ -135,11 +135,11 @@ public function getRemoteAddress(bool $ipToLong = false) $this->remoteAddress = false; return false; - } else { - $this->remoteAddress = $remoteAddress; - - return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } + + $this->remoteAddress = $remoteAddress; + + return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } /** diff --git a/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php b/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php index 25f665ed70e84..20aafb797ce0e 100644 --- a/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php +++ b/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php @@ -9,13 +9,10 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test for - * * @see RemoteAddress */ class RemoteAddressTest extends TestCase @@ -23,24 +20,17 @@ class RemoteAddressTest extends TestCase /** * @var MockObject|HttpRequest */ - protected $_request; - - /** - * @var ObjectManager - */ - protected $_objectManager; + private $requestMock; /** * @inheritdoc */ protected function setUp(): void { - $this->_request = $this->getMockBuilder(HttpRequest::class) + $this->requestMock = $this->getMockBuilder(HttpRequest::class) ->disableOriginalConstructor() - ->setMethods(['getServer']) + ->onlyMethods(['getServer']) ->getMock(); - - $this->_objectManager = new ObjectManager($this); } /** @@ -49,6 +39,7 @@ protected function setUp(): void * @param string|bool $expected * @param bool $ipToLong * @param string[]|null $trustedProxies + * * @return void * @dataProvider getRemoteAddressProvider */ @@ -59,18 +50,16 @@ public function testGetRemoteAddress( bool $ipToLong, array $trustedProxies = null ): void { - $remoteAddress = $this->_objectManager->getObject( - RemoteAddress::class, - [ - 'httpRequest' => $this->_request, - 'alternativeHeaders' => $alternativeHeaders, - 'trustedProxies' => $trustedProxies, - ] + $remoteAddress = new RemoteAddress( + $this->requestMock, + $alternativeHeaders, + $trustedProxies ); - $this->_request->expects($this->any()) - ->method('getServer') + $this->requestMock->method('getServer') ->willReturnMap($serverValueMap); + // Check twice to verify if internal variable is cached correctly + $this->assertEquals($expected, $remoteAddress->getRemoteAddress($ipToLong)); $this->assertEquals($expected, $remoteAddress->getRemoteAddress($ipToLong)); } diff --git a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php index 6b7fcd131ba8b..f702b71378bb9 100644 --- a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php +++ b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php @@ -74,7 +74,7 @@ public function getAttributes($entityType) $attributes[] = $provider->getAttributes($entityType); } - $this->registry[$entityType] = \array_merge(...$attributes); + $this->registry[$entityType] = \array_merge([], ...$attributes); } return $this->registry[$entityType]; diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index 7e484407d7a54..f30d38b6e112e 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php +++ b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php @@ -210,6 +210,6 @@ private function expandSequence($list, $name, $accumulated = []) $allResults[] = $this->expandSequence($list, $relatedName, $accumulated); } $allResults[] = $result; - return array_unique(array_merge(...$allResults)); + return array_unique(array_merge([], ...$allResults)); } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php index b662a2a34c813..b57f4665aff21 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php @@ -239,7 +239,7 @@ protected function resolveArgumentsInRuntime($requestedType, array $parameters, $resolvedArguments[] = $this->getResolvedArgument((string)$requestedType, $parameter, $arguments); } - return empty($resolvedArguments) ? [] : array_merge(...$resolvedArguments); + return array_merge([], ...$resolvedArguments); } /** diff --git a/lib/internal/Magento/Framework/Session/Config.php b/lib/internal/Magento/Framework/Session/Config.php index 296b7944ea4f6..1791ab09156fd 100644 --- a/lib/internal/Magento/Framework/Session/Config.php +++ b/lib/internal/Magento/Framework/Session/Config.php @@ -28,6 +28,18 @@ class Config implements ConfigInterface /** Configuration path for session cache limiter */ const PARAM_SESSION_CACHE_LIMITER = 'session/cache_limiter'; + /** Configuration path for session garbage collection probability */ + private const PARAM_SESSION_GC_PROBABILITY = 'session/gc_probability'; + + /** Configuration path for session garbage collection divisor */ + private const PARAM_SESSION_GC_DIVISOR = 'session/gc_divisor'; + + /** + * Configuration path for session garbage collection max lifetime. + * The number of seconds after which data will be seen as 'garbage'. + */ + private const PARAM_SESSION_GC_MAXLIFETIME = 'session/gc_maxlifetime'; + /** Configuration path for cookie domain */ const XML_PATH_COOKIE_DOMAIN = 'web/cookie/cookie_domain'; @@ -102,6 +114,7 @@ class Config implements ConfigInterface * @param string $scopeType * @param string $lifetimePath * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function __construct( \Magento\Framework\ValidatorFactory $validatorFactory, @@ -149,6 +162,30 @@ public function __construct( $this->setOption('session.cache_limiter', $cacheLimiter); } + /** + * Session garbage collection probability + */ + $gcProbability = $deploymentConfig->get(self::PARAM_SESSION_GC_PROBABILITY); + if ($gcProbability) { + $this->setOption('session.gc_probability', $gcProbability); + } + + /** + * Session garbage collection divisor + */ + $gcDivisor = $deploymentConfig->get(self::PARAM_SESSION_GC_DIVISOR); + if ($gcDivisor) { + $this->setOption('session.gc_divisor', $gcDivisor); + } + + /** + * Session garbage collection max lifetime + */ + $gcMaxlifetime = $deploymentConfig->get(self::PARAM_SESSION_GC_MAXLIFETIME); + if ($gcMaxlifetime) { + $this->setOption('session.gc_maxlifetime', $gcMaxlifetime); + } + /** * Cookie settings: lifetime, path, domain, httpOnly. These govern settings for the session cookie. */ diff --git a/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php b/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php index 4247b7b1aab2f..3d3bb5b6578a9 100644 --- a/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php +++ b/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php @@ -92,10 +92,10 @@ protected function execute() /** * Get foreign keys for tables and columns * - * @param string $refTable - * @param string $refColumn * @param string $targetTable * @param string $targetColumn + * @param string $refTable + * @param string $refColumn * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -110,7 +110,7 @@ protected function getForeignKeys( ); $foreignKeys = array_filter( $foreignKeys, - function ($key) use ($targetColumn, $refTable, $refColumn) { + function ($key) use ($targetColumn, $refTable) { return $key['COLUMN_NAME'] == $targetColumn && $key['REF_TABLE_NAME'] == $refTable; } diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php new file mode 100644 index 0000000000000..3c703c9f037d7 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php @@ -0,0 +1,243 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Test\Unit\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\ConfigurableWYSIWYGValidator; +use Magento\Framework\Validator\HTML\AttributeValidatorInterface; +use Magento\Framework\Validator\HTML\TagValidatorInterface; +use PHPUnit\Framework\TestCase; + +class ConfigurableWYSIWYGValidatorTest extends TestCase +{ + /** + * Configurations to test. + * + * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getConfigurations(): array + { + return [ + 'no-html' => [['div'], [], [], 'just text', true, [], []], + 'allowed-tag' => [['div'], [], [], 'just text and <div>a div</div>', true, [], []], + 'restricted-tag' => [ + ['div', 'p'], + [], + [], + 'text and <p>a p</p>, <div>a div</div>, <tr>a tr</tr>', + false, + [], + [] + ], + 'restricted-tag-wtih-attr' => [ + ['div'], + [], + [], + 'just text and <p class="fake-class">a p</p>', + false, + [], + [] + ], + 'allowed-tag-with-attr' => [ + ['div'], + [], + [], + 'just text and <div class="fake-class">a div</div>', + false, + [], + [] + ], + 'multiple-tags' => [['div', 'p'], [], [], 'just text and <div>a div</div> and <p>a p</p>', true, [], []], + 'tags-with-attrs' => [ + ['div', 'p'], + ['class', 'style'], + [], + 'text and <div class="fake-class">a div</div> and <p style="color: blue">a p</p>', + true, + [], + [] + ], + 'tags-with-restricted-attrs' => [ + ['div', 'p'], + ['class', 'align'], + [], + 'text and <div class="fake-class">a div</div> and <p style="color: blue">a p</p>', + false, + [], + [] + ], + 'tags-with-specific-attrs' => [ + ['div', 'a', 'p'], + ['class'], + ['a' => ['href'], 'div' => ['style']], + '<div class="fake-class" style="color: blue">a div</div>, <a href="/some-path" class="a">an a</a>' + .', <p class="p-class">a p</p>', + true, + [], + [] + ], + 'tags-with-specific-restricted-attrs' => [ + ['div', 'a'], + ['class'], + ['a' => ['href']], + 'text and <div class="fake-class" href="what">a div</div> and <a href="/some-path" class="a">an a</a>', + false, + [], + [] + ], + 'invalid-tag-with-full-config' => [ + ['div', 'a', 'p'], + ['class', 'src'], + ['a' => ['href'], 'div' => ['style']], + '<div class="fake-class" style="color: blue">a div</div>, <a href="/some-path" class="a">an a</a>' + .', <p class="p-class">a p</p>, <img src="test.jpg" />', + false, + [], + [] + ], + 'invalid-html' => [ + ['div', 'a', 'p'], + ['class', 'src'], + ['a' => ['href'], 'div' => ['style']], + 'some </,none-> </html>', + true, + [], + [] + ], + 'invalid-html-with-violations' => [ + ['div', 'a', 'p'], + ['class', 'src'], + ['a' => ['href'], 'div' => ['style']], + 'some </,none-> </html> <tr>some trs</tr>', + false, + [], + [] + ], + 'invalid-html-attributes' => [ + ['div', 'a', 'p'], + ['class', 'src'], + [], + 'some <div class="value">DIV</div>', + false, + ['class' => false], + [] + ], + 'ignored-html-attributes' => [ + ['div', 'a', 'p'], + ['class', 'src'], + [], + 'some <div class="value">DIV</div>', + true, + ['src' => false, 'class' => true], + [] + ], + 'valid-html-attributes' => [ + ['div', 'a', 'p'], + ['class', 'src'], + [], + 'some <div class="value">DIV</div>', + true, + ['src' => true, 'class' => true], + [] + ], + 'invalid-allowed-tag' => [ + ['div'], + ['class', 'src'], + [], + '<div class="some-class" src="some-src">IS A DIV</div>', + false, + [], + ['div' => ['class' => false]] + ], + 'valid-allowed-tag' => [ + ['div'], + ['class', 'src'], + [], + '<div class="some-class">IS A DIV</div>', + true, + [], + ['div' => ['src' => false]] + ] + ]; + } + + /** + * Test different configurations and content. + * + * @param string[] $allowedTags + * @param string[] $allowedAttr + * @param string[][] $allowedTagAttrs + * @param string $html + * @param bool $isValid + * @param bool[] $attributeValidityMap + * @param bool[][] $tagValidators + * @return void + * + * @dataProvider getConfigurations + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function testConfigurations( + array $allowedTags, + array $allowedAttr, + array $allowedTagAttrs, + string $html, + bool $isValid, + array $attributeValidityMap, + array $tagValidators + ): void { + $attributeValidator = $this->getMockForAbstractClass(AttributeValidatorInterface::class); + $attributeValidator->method('validate') + ->willReturnCallback( + function (string $tag, string $attribute) use ($attributeValidityMap): void { + if (array_key_exists($attribute, $attributeValidityMap) && !$attributeValidityMap[$attribute]) { + throw new ValidationException(__('Invalid attribute for %1', $tag)); + } + } + ); + $attrValidators = []; + foreach (array_keys($attributeValidityMap) as $attr) { + $attrValidators[$attr] = [$attributeValidator]; + } + $tagValidatorsMocks = []; + foreach ($tagValidators as $tag => $allowedAttributes) { + $mock = $this->getMockForAbstractClass(TagValidatorInterface::class); + $mock->method('validate') + ->willReturnCallback( + function (string $givenTag, array $attrs) use ($tag, $allowedAttributes): void { + if ($givenTag !== $tag) { + throw new \RuntimeException(); + } + foreach (array_keys($attrs) as $attr) { + if (array_key_exists($attr, $allowedAttributes) && !$allowedAttributes[$attr]) { + throw new ValidationException(__('Invalid tag')); + } + } + } + ); + $tagValidatorsMocks[$tag] = [$mock]; + } + $validator = new ConfigurableWYSIWYGValidator( + $allowedTags, + $allowedAttr, + $allowedTagAttrs, + $attrValidators, + $tagValidatorsMocks + ); + $valid = true; + try { + $validator->validate($html); + } catch (ValidationException $exception) { + $valid = false; + } + + self::assertEquals($isValid, $valid); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php new file mode 100644 index 0000000000000..b705939feec16 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Test\Unit\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\StyleAttributeValidator; +use PHPUnit\Framework\TestCase; + +class StyleAttributeValidatorTest extends TestCase +{ + /** + * Cases for "validate" test. + * + * @return array + */ + public function getAttributes(): array + { + return [ + 'not a style' => ['class', 'value', true], + 'valid style' => ['style', 'color: blue', true], + 'invalid position style' => ['style', 'color: blue; position: absolute; width: 100%', false], + 'another invalid position style' => ['style', 'position: fixed; width: 100%', false], + 'valid position style' => ['style', 'color: blue; position: inherit; width: 100%', true], + 'valid background style' => ['style', 'color: blue; background-position: left; width: 100%', true], + 'invalid opacity style' => ['style', 'color: blue; width: 100%; opacity: 0.5', false], + 'invalid z-index style' => ['style', 'color: blue; width: 100%; z-index: 11', false] + ]; + } + + /** + * Test "validate" method. + * + * @param string $attr + * @param string $value + * @param bool $expectedValid + * @return void + * @dataProvider getAttributes + */ + public function testValidate(string $attr, string $value, bool $expectedValid): void + { + $validator = new StyleAttributeValidator(); + + try { + $validator->validate('does not matter', $attr, $value); + $actuallyValid = true; + } catch (ValidationException $exception) { + $actuallyValid = false; + } + $this->assertEquals($expectedValid, $actuallyValid); + } +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php b/lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php new file mode 100644 index 0000000000000..6426e19a537da --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; + +/** + * Validates HTML attributes content. + */ +interface AttributeValidatorInterface +{ + /** + * Validate attribute. + * + * @param string $tag + * @param string $attributeName + * @param string $value + * @return void + * @throws ValidationException + */ + public function validate(string $tag, string $attributeName, string $value): void; +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php new file mode 100644 index 0000000000000..bfa6bc37600bf --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -0,0 +1,245 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; + +/** + * Validates user HTML based on configuration. + */ +class ConfigurableWYSIWYGValidator implements WYSIWYGValidatorInterface +{ + /** + * @var string[] + */ + private $allowedTags; + + /** + * @var string[] + */ + private $allowedAttributes; + + /** + * @var string[] + */ + private $attributesAllowedByTags; + + /** + * @var AttributeValidatorInterface[][] + */ + private $attributeValidators; + + /** + * @var TagValidatorInterface[][] + */ + private $tagValidators; + + /** + * @param string[] $allowedTags + * @param string[] $allowedAttributes + * @param string[][] $attributesAllowedByTags + * @param AttributeValidatorInterface[][] $attributeValidators + * @param TagValidatorInterface[][] $tagValidators + */ + public function __construct( + array $allowedTags, + array $allowedAttributes = [], + array $attributesAllowedByTags = [], + array $attributeValidators = [], + array $tagValidators = [] + ) { + if (empty(array_filter($allowedTags))) { + throw new \InvalidArgumentException('List of allowed HTML tags cannot be empty'); + } + $this->allowedTags = array_unique($allowedTags); + $this->allowedAttributes = array_unique($allowedAttributes); + $this->attributesAllowedByTags = array_filter( + $attributesAllowedByTags, + function (string $tag) use ($allowedTags): bool { + return in_array($tag, $allowedTags, true); + }, + ARRAY_FILTER_USE_KEY + ); + $this->attributeValidators = $attributeValidators; + $this->tagValidators = $tagValidators; + } + + /** + * @inheritDoc + */ + public function validate(string $content): void + { + if (mb_strlen($content) === 0) { + return; + } + $dom = $this->loadHtml($content); + $xpath = new \DOMXPath($dom); + + $this->validateConfigured($xpath); + $this->callAttributeValidators($xpath); + $this->callTagValidators($xpath); + } + + /** + * Check declarative restrictions + * + * @param \DOMXPath $xpath + * @return void + * @throws ValidationException + */ + private function validateConfigured(\DOMXPath $xpath): void + { + //Validating tags + $found = $xpath->query( + '//*[' + . implode( + ' and ', + array_map( + function (string $tag): string { + return "name() != '$tag'"; + }, + array_merge($this->allowedTags, ['body', 'html']) + ) + ) + .']' + ); + if (count($found)) { + throw new ValidationException( + __('Allowed HTML tags are: %1', implode(', ', $this->allowedTags)) + ); + } + + //Validating attributes + if ($this->attributesAllowedByTags) { + foreach ($this->allowedTags as $tag) { + $allowed = [$this->allowedAttributes]; + if (!empty($this->attributesAllowedByTags[$tag])) { + $allowed[] = $this->attributesAllowedByTags[$tag]; + } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $allowed = array_unique(array_merge(...$allowed)); + $allowedQuery = ''; + if ($allowed) { + $allowedQuery = '[' + . implode( + ' and ', + array_map( + function (string $attribute): string { + return "name() != '$attribute'"; + }, + $allowed + ) + ) + .']'; + } + $found = $xpath->query("//$tag/@*$allowedQuery"); + if (count($found)) { + throw new ValidationException( + __('Allowed HTML attributes for tag "%1" are: %2', $tag, implode(',', $allowed)) + ); + } + } + } else { + $allowed = ''; + if ($this->allowedAttributes) { + $allowed = '[' + . implode( + ' and ', + array_map( + function (string $attribute): string { + return "name() != '$attribute'"; + }, + $this->allowedAttributes + ) + ) + .']'; + } + $found = $xpath->query("//@*$allowed"); + if (count($found)) { + throw new ValidationException( + __('Allowed HTML attributes are: %1', implode(',', $this->allowedAttributes)) + ); + } + } + } + + /** + * Validate allowed HTML attributes' content. + * + * @param \DOMXPath $xpath + * @throws ValidationException + * @return void + */ + private function callAttributeValidators(\DOMXPath $xpath): void + { + if ($this->attributeValidators) { + foreach ($this->attributeValidators as $attr => $validators) { + $found = $xpath->query("//@*[name() = '$attr']"); + foreach ($found as $attribute) { + foreach ($validators as $validator) { + $validator->validate($attribute->parentNode->tagName, $attribute->name, $attribute->value); + } + } + } + } + } + + /** + * Validate allowed tags. + * + * @param \DOMXPath $xpath + * @return void + * @throws ValidationException + */ + private function callTagValidators(\DOMXPath $xpath): void + { + if ($this->tagValidators) { + foreach ($this->tagValidators as $tag => $validators) { + $found = $xpath->query("//*[name() = '$tag']"); + /** @var \DOMElement $tagNode */ + foreach ($found as $tagNode) { + $attributes = []; + if ($tagNode->hasAttributes()) { + /** @var \DOMAttr $attributeNode */ + foreach ($tagNode->attributes as $attributeNode) { + $attributes[$attributeNode->name] = $attributeNode->value; + } + } + foreach ($validators as $validator) { + $validator->validate($tagNode->tagName, $attributes, $tagNode->textContent, $this); + } + } + } + } + } + + /** + * Load DOM. + * + * @param string $content + * @return \DOMDocument + * @throws ValidationException + */ + private function loadHtml(string $content): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + set_error_handler( + function () use (&$loaded) { + $loaded = false; + } + ); + $loaded = $dom->loadHTML("<html><body>$content</body></html>"); + restore_error_handler(); + if (!$loaded) { + throw new ValidationException(__('Invalid HTML content provided')); + } + + return $dom; + } +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php b/lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php new file mode 100644 index 0000000000000..4b5ccc9e32863 --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; + +/** + * Validates "style" attribute. + */ +class StyleAttributeValidator implements AttributeValidatorInterface +{ + /** + * @inheritDoc + */ + public function validate(string $tag, string $attributeName, string $value): void + { + if ($attributeName !== 'style' || !$value) { + return; + } + + if (preg_match('/([^\-]position\s*?\:\s*?[^i\s][^n\s]\w)|(opacity)|(z-index)/ims', " $value")) { + throw new ValidationException(__('HTML attribute "style" contains restricted styles')); + } + } +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php b/lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php new file mode 100644 index 0000000000000..d81172edc87c9 --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; + +/** + * Validates tag for user HTML content. + */ +interface TagValidatorInterface +{ + /** + * Validate a tag. + * + * @param string $tag + * @param string[] $attributes + * @param string $value + * @param WYSIWYGValidatorInterface $recursiveValidator + * @return void + * @throws ValidationException + */ + public function validate( + string $tag, + array $attributes, + string $value, + WYSIWYGValidatorInterface $recursiveValidator + ): void; +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php b/lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php new file mode 100644 index 0000000000000..8045bc6a86c0b --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Validator\HTML; + +use Magento\Framework\Validation\ValidationException; + +/** + * Validates user HTML. + */ +interface WYSIWYGValidatorInterface +{ + /** + * Validate user HTML content. + * + * @param string $content + * @throws ValidationException + */ + public function validate(string $content): void; +} diff --git a/lib/web/css/source/components/_modals.less b/lib/web/css/source/components/_modals.less index 58c9c0674b6ad..8513db545f1ec 100644 --- a/lib/web/css/source/components/_modals.less +++ b/lib/web/css/source/components/_modals.less @@ -103,10 +103,6 @@ &.confirm { .modal-inner-wrap { .lib-css(max-width, @modal-popup-confirm__width); - - .modal-content { - padding-right: 7rem; - } } } diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js index 4393b6c882039..d74838b0c26bf 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js @@ -205,6 +205,7 @@ define([ plugins: this.config.tinymce4.plugins, toolbar: this.config.tinymce4.toolbar, adapter: this, + 'body_id': 'html-body', /** * @param {Object} editor diff --git a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php index 173ea9e49a8a4..8e64aae20573c 100644 --- a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +++ b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php @@ -7,6 +7,7 @@ namespace Magento\Setup\Console\Command; use Magento\Framework\Setup\ConsoleLogger; +use Magento\Framework\Validation\ValidationException; use Magento\Setup\Model\AdminAccount; use Magento\Setup\Model\InstallerFactory; use Magento\User\Model\UserValidationRules; @@ -81,7 +82,7 @@ protected function interact(InputInterface $input, OutputInterface $output) $question = new Question('<question>Admin password:</question> ', ''); $question->setHidden(true); - $question->setValidator(function ($value) use ($output) { + $question->setValidator(function ($value) { $user = new \Magento\Framework\DataObject(); $user->setPassword($value); @@ -90,7 +91,7 @@ protected function interact(InputInterface $input, OutputInterface $output) $validator->isValid($user); foreach ($validator->getMessages() as $message) { - throw new \Exception($message); + throw new ValidationException(__($message)); } return $value; @@ -143,7 +144,7 @@ private function addNotEmptyValidator(Question $question) { $question->setValidator(function ($value) { if (trim($value) == '') { - throw new \Exception('The value cannot be empty'); + throw new ValidationException(__('The value cannot be empty')); } return $value; diff --git a/setup/src/Magento/Setup/Console/Command/RollbackCommand.php b/setup/src/Magento/Setup/Console/Command/RollbackCommand.php index a9138b9faefa1..e114c84ba79bc 100644 --- a/setup/src/Magento/Setup/Console/Command/RollbackCommand.php +++ b/setup/src/Magento/Setup/Console/Command/RollbackCommand.php @@ -80,7 +80,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ protected function configure() { @@ -111,7 +111,7 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritDoc */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -122,8 +122,9 @@ protected function execute(InputInterface $input, OutputInterface $output) // we must have an exit code higher than zero to indicate something was wrong return \Magento\Framework\Console\Cli::RETURN_FAILURE; } - $returnValue = $this->maintenanceModeEnabler->executeInMaintenanceMode( - function () use ($input, $output, &$returnValue) { + + return $this->maintenanceModeEnabler->executeInMaintenanceMode( + function () use ($input, $output) { try { $helper = $this->getHelper('question'); $question = new ConfirmationQuestion( @@ -152,7 +153,6 @@ function () use ($input, $output, &$returnValue) { $output, false ); - return $returnValue; } /** diff --git a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php index 60cfcbb67c217..43f85d092b0a7 100644 --- a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php +++ b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php @@ -565,6 +565,7 @@ private function createBlock( ) { $indentLength = 0; $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); + $lineIndentation = ''; if (null !== $type) { $type = sprintf('[%s] ', $type); $indentLength = strlen($type); @@ -605,7 +606,7 @@ private function getBlockLines( int $prefixLength, int $indentLength ) { - $lines = [[]]; + $lines = []; foreach ($messages as $key => $message) { $message = OutputFormatter::escape($message); $wordwrap = wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true); @@ -614,7 +615,7 @@ private function getBlockLines( $lines[][] = ''; } } - $lines = array_merge(...$lines); + $lines = array_merge([], ...$lines); return $lines; } diff --git a/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php b/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php index 26e7857703b4f..56263d0ec0adb 100644 --- a/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php @@ -91,7 +91,7 @@ function ($values, $index) use ($optionCount, $data, $type) { ); $attribute['optionvisual']['value'] = array_reduce( range(1, $optionCount), - function ($values, $index) use ($optionCount) { + function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; return $values; }, @@ -129,6 +129,7 @@ private function generateSwatchImage($data) $this->imagesGenerator = $this->imagesGeneratorFactory->create(); } + // phpcs:ignore Magento2.Security.InsecureFunction $imageName = md5($data) . '.jpg'; $this->imagesGenerator->generate([ 'image-width' => self::GENERATED_SWATCH_WIDTH, diff --git a/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php b/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php index f143685f1903d..671627bcea8a9 100644 --- a/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php @@ -77,7 +77,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function execute() { @@ -93,7 +93,7 @@ public function execute() } /** - * {@inheritdoc} + * @inheritDoc */ public function getActionTitle() { @@ -101,7 +101,7 @@ public function getActionTitle() } /** - * {@inheritdoc} + * @inheritDoc */ public function introduceParamLabels() { @@ -109,6 +109,8 @@ public function introduceParamLabels() } /** + * Generate Attribute + * * @param int $optionCount * @return void */ @@ -169,7 +171,7 @@ function ($values, $index) use ($optionCount) { ); $data['optionvisual']['value'] = array_reduce( range(1, $optionCount), - function ($values, $index) use ($optionCount) { + function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; return $values; }, @@ -194,6 +196,8 @@ function ($values, $index) use ($optionCount) { } /** + * Get attribute code + * * @return string */ private function getAttributeCode() diff --git a/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php b/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php index 27f0c7e8e616f..d162d07b38cf8 100644 --- a/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php +++ b/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php @@ -95,7 +95,7 @@ public static function getCircularDependenciesReportBuilder() self::$circularDependenciesReportBuilder = new CircularReport\Builder( self::getComposerJsonParser(), new CircularReport\Writer(self::getCsvWriter()), - new CircularTool([], null) + new CircularTool() ); } return self::$circularDependenciesReportBuilder; diff --git a/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php index acb55e29afddd..7355ac30ac59d 100644 --- a/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php +++ b/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php @@ -175,7 +175,7 @@ protected function _fetchMissingExtensionAttributesClasses($reflectionClass, $fi */ public function collectEntities(array $files) { - $output = [[]]; + $output = []; foreach ($files as $file) { $classes = $this->getDeclaredClasses($file); foreach ($classes as $className) { @@ -184,7 +184,7 @@ public function collectEntities(array $files) $output[] = $this->_fetchMissingExtensionAttributesClasses($reflectionClass, $file); } } - return array_unique(array_merge(...$output)); + return array_unique(array_merge([], ...$output)); } /** diff --git a/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php b/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php index cf38fd70884f3..ec62ab8b84482 100644 --- a/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php +++ b/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Setup\Module\I18n\Parser\Adapter; +use Exception; use Magento\Email\Model\Template\Filter; /** @@ -16,17 +19,30 @@ class Html extends AbstractAdapter * Covers * <span><!-- ko i18n: 'Next'--><!-- /ko --></span> * <th class="col col-method" data-bind="i18n: 'Select Method'"></th> + * @deprecated Not used anymore because of newly introduced constant + * @see self::HTML_REGEX_LIST */ const HTML_FILTER = "/i18n:\s?'(?<value>[^'\\\\]*(?:\\\\.[^'\\\\]*)*)'/i"; + private const HTML_REGEX_LIST = [ + // <span><!-- ko i18n: 'Next'--><!-- /ko --></span> + // <th class="col col-method" data-bind="i18n: 'Select Method'"></th> + "/i18n:\s?'(?<value>[^'\\\\]*(?:\\\\.[^'\\\\]*)*)'/i", + // <translate args="'System Messages'"/> + // <span translate="'Examples'"></span> + "/translate( args|)=\"'(?<value>[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)'\"/i" + ]; + /** * @inheritdoc */ protected function _parse() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $data = file_get_contents($this->_file); if ($data === false) { - throw new \Exception('Failed to load file from disk.'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception('Failed to load file from disk.'); } $results = []; @@ -37,15 +53,19 @@ protected function _parse() if (preg_match(Filter::TRANS_DIRECTIVE_REGEX, $results[$i][2], $directive) !== 1) { continue; } + $quote = $directive[1]; $this->_addPhrase($quote . $directive[2] . $quote); } } - preg_match_all(self::HTML_FILTER, $data, $results, PREG_SET_ORDER); - for ($i = 0, $count = count($results); $i < $count; $i++) { - if (!empty($results[$i]['value'])) { - $this->_addPhrase($results[$i]['value']); + foreach (self::HTML_REGEX_LIST as $regex) { + preg_match_all($regex, $data, $results, PREG_SET_ORDER); + + for ($i = 0, $count = count($results); $i < $count; $i++) { + if (!empty($results[$i]['value'])) { + $this->_addPhrase($results[$i]['value']); + } } } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php index 15c442e9bac98..d7a2f0b4a9397 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php @@ -7,33 +7,25 @@ namespace Magento\Setup\Test\Unit\Module\I18n\Parser\Adapter; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Setup\Module\I18n\Parser\Adapter\Html; use PHPUnit\Framework\TestCase; class HtmlTest extends TestCase { /** - * @var string - */ - protected $_testFile; - - /** - * @var int + * @var Html */ - protected $_stringsCount; + private $model; /** - * @var Html + * @var string */ - protected $_adapter; + private $testFile; protected function setUp(): void { - $this->_testFile = str_replace('\\', '/', realpath(dirname(__FILE__))) . '/_files/email.html'; - $this->_stringsCount = count(file($this->_testFile)); - - $this->_adapter = (new ObjectManager($this))->getObject(Html::class); + $this->testFile = str_replace('\\', '/', realpath(__DIR__)) . '/_files/email.html'; + $this->model = new Html(); } public function testParse() @@ -41,68 +33,80 @@ public function testParse() $expectedResult = [ [ 'phrase' => 'Phrase 1', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '\'', ], [ 'phrase' => 'Phrase 2 with %a_lot of extra info for the brilliant %customer_name.', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '"', ], [ 'phrase' => 'This is test data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data at right side of attr', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is \\\' test \\\' data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is \\" test \\" data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data with a quote after', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data with space after ', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => '\\\'', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => '\\\\\\\\ ', - 'file' => $this->_testFile, + 'file' => $this->testFile, + 'line' => '', + 'quote' => '', + ], + [ + 'phrase' => 'This is test content in translate tag', + 'file' => $this->testFile, + 'line' => '', + 'quote' => '', + ], + [ + 'phrase' => 'This is test content in translate attribute', + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], ]; - $this->_adapter->parse($this->_testFile); + $this->model->parse($this->testFile); - $this->assertEquals($expectedResult, $this->_adapter->getPhrases()); + $this->assertEquals($expectedResult, $this->model->getPhrases()); } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html index 90579b48a07b5..f5603768ef306 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html +++ b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html @@ -29,5 +29,7 @@ <label data-bind="i18n: ''"></label> <label data-bind="i18n: '\''"></label> <label data-bind="i18n: '\\\\ '"></label> + <span><translate args="'This is test content in translate tag'" /></span> + <span translate="'This is test content in translate attribute'"></span> </body> </html>