From e286201273ed128bad91fb90fbfbb4df245d18bd Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Sat, 23 Nov 2019 16:38:56 -0600 Subject: [PATCH 1/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- patches.json | 33 + ...ulti-site_configuration_issue__2.2.4.patch | 16 + ...nnot_change_the_applied_theme__2.2.5.patch | 17 + ...retrieving_product_attributes__2.2.5.patch | 185 ++ ...91__customer_attributes_issue__2.2.6.patch | 1592 +++++++++++++++++ ...d_is_removed_by_authorize_net__2.2.0.patch | 337 ++++ ...e_view_scope_in async_web_api__2.3.1.patch | 934 ++++++++++ ...e_view_scope_in async_web_api__2.3.2.patch | 948 ++++++++++ .../PRODSECBUG-2198__preauth_sql__2.1.4.patch | 16 + .../PRODSECBUG-2198__preauth_sql__2.2.0.patch | 96 + .../PRODSECBUG-2198__preauth_sql__2.3.0.patch | 312 ++++ ...233__fix_xss_in_order_history__2.2.0.patch | 216 +++ ...233__fix_xss_in_order_history__2.3.0.patch | 217 +++ ...32__admin_path_disclosure_bug__2.1.4.patch | 35 + ...32__admin_path_disclosure_bug__2.2.0.patch | 36 + ...32__admin_path_disclosure_bug__2.3.0.patch | 36 + 16 files changed, 5026 insertions(+) create mode 100644 patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch create mode 100644 patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch create mode 100644 patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch create mode 100644 patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch create mode 100644 patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch create mode 100644 patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch create mode 100644 patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch create mode 100644 patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch create mode 100644 patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch create mode 100644 patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch create mode 100644 patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch create mode 100644 patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch create mode 100644 patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch create mode 100644 patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch create mode 100644 patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch diff --git a/patches.json b/patches.json index 0b37292..01f6e38 100644 --- a/patches.json +++ b/patches.json @@ -205,6 +205,39 @@ "Fix pagebuilder module": { "2.3.1": "MDVA-22979__fix_pagebuilder_module__2.3.1.patch", "2.3.2": "MDVA-22979__fix_pagebuilder_module__2.3.2.patch" + }, + "Fix XSS in order history": { + "2.2.0 - 2.2.8": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch", + "2.3.0 - 2.3.1": "PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch" + }, + "Pass Store View scope in the Async/Bulk Web API": { + "2.3.1": "MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch", + "2.3.2": "MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch" + }, + "Admin path disclosure bug": { + ">=2.1.4 <2.2.0": "PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch", + ">=2.2.0 <2.3.0": "PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch", + "2.3.0 - 2.3.2": "PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch" + }, + "Pre-auth SQL Injection": { + "2.1.4 - 2.1.17": "PRODSECBUG-2198__preauth_sql__2.1.4.patch", + "2.2.0 - 2.2.7": "PRODSECBUG-2198__preauth_sql__2.2.0.patch", + "2.3.0": "PRODSECBUG-2198__preauth_sql__2.3.0.patch" + }, + "Transaction MD5 hash field is removed by Authorize.net": { + "2.2.0 - 2.2.7": "MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch" + }, + "Customer attributes issue": { + "2.2.6": "MAGETWO-95591__customer_attributes_issue__2.2.6.patch" + }, + "Optimize retrieving product attributes": { + "2.2.5": "MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch" + }, + "Cannot change the applied theme": { + "2.2.4 - 2.2.5": "MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch" + }, + "Fix for multi-site configuration issue": { + "2.2.4": "MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch" } }, "monolog/monolog": { diff --git a/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch b/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch new file mode 100644 index 0000000..8fdf994 --- /dev/null +++ b/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch @@ -0,0 +1,16 @@ +diff --git a/app/etc/di.xml b/app/etc/di.xml +index de50840..4eaa739 100644 +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -232,6 +233,11 @@ + Magento\Backend\App\Request\PathInfoProcessor\Proxy + + ++ ++ ++ Magento\Framework\Session\Config\ConfigInterface\Proxy ++ ++ + + + diff --git a/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch b/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch new file mode 100644 index 0000000..659da92 --- /dev/null +++ b/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch @@ -0,0 +1,17 @@ +diff --git a/vendor/magento/module-email/Model/AbstractTemplate.php b/vendor/magento/module-email/Model/AbstractTemplate.php +index fa9d28074bf8..81e993fad76d 100644 +--- a/vendor/magento/module-email/Model/AbstractTemplate.php ++++ b/vendor/magento/module-email/Model/AbstractTemplate.php +@@ -534,10 +534,9 @@ protected function cancelDesignConfig() + */ + public function setForcedArea($templateId) + { +- if ($this->area) { +- throw new \LogicException(__('Area is already set')); ++ if (!isset($this->area)) { ++ $this->area = $this->emailConfig->getTemplateArea($templateId); + } +- $this->area = $this->emailConfig->getTemplateArea($templateId); + return $this; + } + diff --git a/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch b/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch new file mode 100644 index 0000000..b7a4f65 --- /dev/null +++ b/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch @@ -0,0 +1,185 @@ +diff --git a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +index 237ac96e56ec..24c3879d947b 100644 +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -12,6 +12,7 @@ + use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; + use Magento\Framework\Api\AttributeValueFactory; + use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\DataObject\IdentityInterface; + use Magento\Framework\Pricing\SaleableInterface; + +@@ -270,6 +271,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface ++ * @deprecated Not used anymore due to performance issue (loaded all product attributes) + */ + protected $metadataService; + +@@ -346,6 +348,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + protected $linkTypeProvider; + ++ /** ++ * @var \Magento\Eav\Model\Config ++ */ ++ private $eavConfig; ++ + /** + * Product constructor. + * @param \Magento\Framework\Model\Context $context +@@ -383,7 +390,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor + * @param array $data +- * ++ * @param \Magento\Eav\Model\Config|null $config + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +@@ -422,7 +429,8 @@ public function __construct( + EntryConverterPool $mediaGalleryEntryConverterPool, + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor, +- array $data = [] ++ array $data = [], ++ \Magento\Eav\Model\Config $config = null + ) { + $this->metadataService = $metadataService; + $this->_itemOptionFactory = $itemOptionFactory; +@@ -461,6 +469,7 @@ public function __construct( + $resourceCollection, + $data + ); ++ $this->eavConfig = $config ?? ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class); + } + + /** +@@ -474,12 +483,18 @@ protected function _construct() + } + + /** +- * {@inheritdoc} ++ * Get a list of custom attribute codes that belongs to product attribute set. If attribute set not specified for ++ * product will return all attribute codes ++ * ++ * @return string[] + */ + protected function getCustomAttributesCodes() + { + if ($this->customAttributesCodes === null) { +- $this->customAttributesCodes = $this->getEavAttributesCodes($this->metadataService); ++ $this->customAttributesCodes = array_keys($this->eavConfig->getEntityAttributes( ++ self::ENTITY, ++ $this ++ )); + $this->customAttributesCodes = array_diff($this->customAttributesCodes, $this->interfaceAttributes); + } + return $this->customAttributesCodes; +diff --git a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +index 4dae4ec68efa..ff4d2f93c912 100644 +--- a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php ++++ b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +@@ -58,7 +58,9 @@ public function afterExecute(ReadSnapshot $subject, array $entityData, $entityTy + $globalAttributes = []; + $attributesMap = []; + $eavEntityType = $metadata->getEavEntityType(); +- $attributes = (null === $eavEntityType) ? [] : $this->config->getEntityAttributes($eavEntityType); ++ $attributes = null === $eavEntityType ++ ? [] ++ : $this->config->getEntityAttributes($eavEntityType, new \Magento\Framework\DataObject($entityData)); + + /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ + foreach ($attributes as $attribute) { +diff --git a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php +index 9febea9b7b2b..cd2fe7477ca6 100644 +--- a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php ++++ b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php +@@ -5,6 +5,7 @@ + */ + namespace Magento\Eav\Model\ResourceModel; + ++use Magento\Framework\DataObject; + use Magento\Framework\EntityManager\MetadataPool; + use Magento\Framework\EntityManager\Operation\AttributeInterface; + use Magento\Framework\Model\Entity\ScopeInterface; +@@ -59,13 +60,29 @@ public function __construct( + * @param string $entityType + * @return \Magento\Eav\Api\Data\AttributeInterface[] + * @throws \Exception if for unknown entity type ++ * @deprecated Not used anymore ++ * @see ReadHandler::getEntityAttributes + */ + protected function getAttributes($entityType) + { + $metadata = $this->metadataPool->getMetadata($entityType); + $eavEntityType = $metadata->getEavEntityType(); +- $attributes = (null === $eavEntityType) ? [] : $this->config->getAttributes($eavEntityType); +- return $attributes; ++ return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType); ++ } ++ ++ /** ++ * Get attribute of given entity type ++ * ++ * @param string $entityType ++ * @param DataObject $entity ++ * @return \Magento\Eav\Api\Data\AttributeInterface[] ++ * @throws \Exception if for unknown entity type ++ */ ++ private function getEntityAttributes(string $entityType, DataObject $entity): array ++ { ++ $metadata = $this->metadataPool->getMetadata($entityType); ++ $eavEntityType = $metadata->getEavEntityType(); ++ return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType, $entity); + } + + /** +@@ -105,7 +122,7 @@ public function execute($entityType, $entityData, $arguments = []) + $selects = []; + + /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ +- foreach ($this->getAttributes($entityType) as $attribute) { ++ foreach ($this->getEntityAttributes($entityType, new DataObject($entityData)) as $attribute) { + if (!$attribute->isStatic()) { + $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); + $attributesMap[$attribute->getAttributeId()] = $attribute->getAttributeCode(); +diff --git a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php +index 0569a249260f..4b9dbab7faaa 100644 +--- a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php ++++ b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php +@@ -69,7 +69,7 @@ public function beforeGetImage( + && ($location == self::CATEGORY_PAGE_GRID_LOCATION || $location == self::CATEGORY_PAGE_LIST_LOCATION)) { + $request = $this->request->getParams(); + if (is_array($request)) { +- $filterArray = $this->getFilterArray($request); ++ $filterArray = $this->getFilterArray($request, $product); + if (!empty($filterArray)) { + $product = $this->loadSimpleVariation($product, $filterArray); + } +@@ -99,16 +99,18 @@ protected function loadSimpleVariation(\Magento\Catalog\Model\Product $parentPro + * Get filters from request + * + * @param array $request ++ * @param \Magento\Catalog\Model\Product $product + * @return array + */ +- protected function getFilterArray(array $request) ++ private function getFilterArray(array $request, \Magento\Catalog\Model\Product $product) + { + $filterArray = []; +- $attributeCodes = $this->eavConfig->getEntityAttributeCodes(\Magento\Catalog\Model\Product::ENTITY); ++ $attributes = $this->eavConfig->getEntityAttributes(\Magento\Catalog\Model\Product::ENTITY, $product); ++ + foreach ($request as $code => $value) { +- if (in_array($code, $attributeCodes)) { +- $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $code); +- if ($attribute->getId() && $this->canReplaceImageWithSwatch($attribute)) { ++ if (isset($attributes[$code])) { ++ $attribute = $attributes[$code]; ++ if ($this->canReplaceImageWithSwatch($attribute)) { + $filterArray[$code] = $value; + } + } diff --git a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch new file mode 100644 index 0000000..2554dc9 --- /dev/null +++ b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch @@ -0,0 +1,1592 @@ +diff --git a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php +index b9f9b739f4fa..fcd0fbc03278 100644 +--- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php ++++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php +@@ -18,6 +18,8 @@ + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator; + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; + use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Serialize\Serializer\FormData; + use Magento\Framework\Cache\FrontendInterface; + use Magento\Framework\Controller\ResultFactory; + use Magento\Framework\Controller\Result\Json; +@@ -68,6 +70,11 @@ class Save extends Attribute + */ + private $layoutFactory; + ++ /** ++ * @var FormData ++ */ ++ private $formDataSerializer; ++ + /** + * @param Context $context + * @param FrontendInterface $attributeLabelCache +@@ -80,6 +87,7 @@ class Save extends Attribute + * @param FilterManager $filterManager + * @param Product $productHelper + * @param LayoutFactory $layoutFactory ++ * @param FormData|null $formDataSerializer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -93,7 +101,8 @@ public function __construct( + CollectionFactory $groupCollectionFactory, + FilterManager $filterManager, + Product $productHelper, +- LayoutFactory $layoutFactory ++ LayoutFactory $layoutFactory, ++ FormData $formDataSerializer = null + ) { + parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); + $this->buildFactory = $buildFactory; +@@ -103,19 +112,37 @@ public function __construct( + $this->validatorFactory = $validatorFactory; + $this->groupCollectionFactory = $groupCollectionFactory; + $this->layoutFactory = $layoutFactory; ++ $this->formDataSerializer = $formDataSerializer ?? ObjectManager::getInstance()->get(FormData::class); + } + + /** +- * @return Redirect ++ * @inheritdoc ++ * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function execute() + { ++ try { ++ $optionData = $this->formDataSerializer->unserialize( ++ $this->getRequest()->getParam('serialized_options', '[]') ++ ); ++ } catch (\InvalidArgumentException $e) { ++ $message = __("The attribute couldn't be saved due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."); ++ $this->messageManager->addErrorMessage($message); ++ ++ return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); ++ } ++ + $data = $this->getRequest()->getPostValue(); ++ $data = array_replace_recursive( ++ $data, ++ $optionData ++ ); ++ + if ($data) { +- $this->preprocessOptionsData($data); + $setId = $this->getRequest()->getParam('set'); + + $attributeSet = null; +@@ -124,7 +151,7 @@ public function execute() + $name = trim($name); + + try { +- /** @var $attributeSet Set */ ++ /** @var Set $attributeSet */ + $attributeSet = $this->buildFactory->create() + ->setEntityTypeId($this->_entityTypeId) + ->setSkeletonId($setId) +@@ -147,7 +174,7 @@ public function execute() + + $attributeId = $this->getRequest()->getParam('attribute_id'); + +- /** @var $model ProductAttributeInterface */ ++ /** @var ProductAttributeInterface $model */ + $model = $this->attributeFactory->create(); + if ($attributeId) { + $model->load($attributeId); +@@ -180,7 +207,7 @@ public function execute() + + //validate frontend_input + if (isset($data['frontend_input'])) { +- /** @var $inputType Validator */ ++ /** @var Validator $inputType */ + $inputType = $this->validatorFactory->create(); + if (!$inputType->isValid($data['frontend_input'])) { + foreach ($inputType->getMessages() as $message) { +@@ -313,28 +340,8 @@ public function execute() + } + + /** +- * Extract options data from serialized options field and append to data array. +- * +- * This logic is required to overcome max_input_vars php limit +- * that may vary and/or be inaccessible to change on different instances. ++ * Provides an initialized Result object. + * +- * @param array $data +- * @return void +- */ +- private function preprocessOptionsData(&$data) +- { +- if (isset($data['serialized_options'])) { +- $serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY); +- foreach ($serializedOptions as $serializedOption) { +- $option = []; +- parse_str($serializedOption, $option); +- $data = array_replace_recursive($data, $option); +- } +- } +- unset($data['serialized_options']); +- } +- +- /** + * @param string $path + * @param array $params + * @param array $response +diff --git a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php +index 7fe012a87d92..e56428a1ae77 100644 +--- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php ++++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php +@@ -6,8 +6,15 @@ + */ + namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; + ++use Magento\Framework\Serialize\Serializer\FormData; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\DataObject; + ++/** ++ * Product attribute validate controller. ++ * ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ */ + class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute + { + const DEFAULT_MESSAGE_KEY = 'message'; +@@ -27,6 +34,11 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute + */ + private $multipleAttributeList; + ++ /** ++ * @var FormData ++ */ ++ private $formDataSerializer; ++ + /** + * Constructor + * +@@ -37,6 +49,7 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute + * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param array $multipleAttributeList ++ * @param FormData|null $formDataSerializer + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, +@@ -45,16 +58,19 @@ public function __construct( + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Framework\View\LayoutFactory $layoutFactory, +- array $multipleAttributeList = [] ++ array $multipleAttributeList = [], ++ FormData $formDataSerializer = null + ) { + parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); + $this->resultJsonFactory = $resultJsonFactory; + $this->layoutFactory = $layoutFactory; + $this->multipleAttributeList = $multipleAttributeList; ++ $this->formDataSerializer = $formDataSerializer ?? ObjectManager::getInstance()->get(FormData::class); + } + + /** +- * @return \Magento\Framework\Controller\ResultInterface ++ * @inheritdoc ++ * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ +@@ -62,6 +78,16 @@ public function execute() + { + $response = new DataObject(); + $response->setError(false); ++ try { ++ $optionsData = $this->formDataSerializer->unserialize( ++ $this->getRequest()->getParam('serialized_options', '[]') ++ ); ++ } catch (\InvalidArgumentException $e) { ++ $message = __("The attribute couldn't be validated due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."); ++ $this->setMessageToResponse($response, [$message]); ++ $response->setError(true); ++ } + + $attributeCode = $this->getRequest()->getParam('attribute_code'); + $frontendLabel = $this->getRequest()->getParam('frontend_label'); +@@ -101,10 +127,10 @@ public function execute() + } + + $multipleOption = $this->getRequest()->getParam("frontend_input"); +- $multipleOption = null == $multipleOption ? 'select' : $multipleOption; ++ $multipleOption = (null === $multipleOption) ? 'select' : $multipleOption; + + if (isset($this->multipleAttributeList[$multipleOption]) && !(null == ($multipleOption))) { +- $options = $this->getRequest()->getParam($this->multipleAttributeList[$multipleOption]); ++ $options = $optionsData[$this->multipleAttributeList[$multipleOption]] ?? null; + $this->checkUniqueOption( + $response, + $options +@@ -122,7 +148,8 @@ public function execute() + } + + /** +- * Throws Exception if not unique values into options ++ * Throws Exception if not unique values into options. ++ * + * @param array $optionsValues + * @param array $deletedOptions + * @return bool +@@ -156,6 +183,8 @@ private function setMessageToResponse($response, $messages) + } + + /** ++ * Performs checking the uniqueness of the attribute options. ++ * + * @param DataObject $response + * @param array|null $options + * @return $this +diff --git a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +index f493cbc88f18..a1aaab0995d7 100644 +--- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php ++++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +@@ -3,8 +3,10 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; + ++use Magento\Catalog\Api\Data\ProductAttributeInterface; + use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; + use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; + use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; +@@ -13,11 +15,16 @@ + use Magento\Eav\Api\Data\AttributeSetInterface; + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; + use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; ++use Magento\Framework\Message\ManagerInterface; ++use Magento\Framework\Serialize\Serializer\FormData; ++use Magento\Framework\Controller\ResultFactory; + use Magento\Framework\Filter\FilterManager; + use Magento\Catalog\Helper\Product as ProductHelper; ++use Magento\Framework\View\Element\Messages; + use Magento\Framework\View\LayoutFactory; + use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator as InputTypeValidator; ++use Magento\Framework\View\LayoutInterface; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -79,6 +86,21 @@ class SaveTest extends AttributeTest + */ + protected $inputTypeValidatorMock; + ++ /** ++ * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $messageManagerMock; ++ ++ /** ++ * @var FormData|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $formDataSerializerMock; ++ ++ /** ++ * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $productAttributeMock; ++ + protected function setUp() + { + parent::setUp(); +@@ -108,6 +130,7 @@ protected function setUp() + ->disableOriginalConstructor() + ->getMock(); + $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) ++ ->setMethods(['setData', 'setPath']) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) +@@ -119,6 +142,15 @@ protected function setUp() + $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) + ->disableOriginalConstructor() + ->getMock(); ++ $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) ++ ->setMethods(['getId', 'get']) ++ ->getMockForAbstractClass(); + + $this->buildFactoryMock->expects($this->any()) + ->method('create') +@@ -126,6 +158,9 @@ protected function setUp() + $this->validatorFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->inputTypeValidatorMock); ++ $this->attributeFactoryMock ++ ->method('create') ++ ->willReturn($this->productAttributeMock); + } + + /** +@@ -135,6 +170,7 @@ protected function getModel() + { + return $this->objectManager->getObject(Save::class, [ + 'context' => $this->contextMock, ++ 'messageManager' => $this->messageManagerMock, + 'attributeLabelCache' => $this->attributeLabelCacheMock, + 'coreRegistry' => $this->coreRegistryMock, + 'resultPageFactory' => $this->resultPageFactoryMock, +@@ -145,11 +181,22 @@ protected function getModel() + 'validatorFactory' => $this->validatorFactoryMock, + 'groupCollectionFactory' => $this->groupCollectionFactoryMock, + 'layoutFactory' => $this->layoutFactoryMock, ++ 'formDataSerializer' => $this->formDataSerializerMock, + ]); + } + + public function testExecuteWithEmptyData() + { ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['isAjax', null, null], ++ ['serialized_options', '[]', ''], ++ ]); ++ $this->formDataSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with('') ++ ->willReturn([]); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn([]); +@@ -170,6 +217,22 @@ public function testExecute() + 'frontend_input' => 'test_frontend_input', + ]; + ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['isAjax', null, null], ++ ['serialized_options', '[]', ''], ++ ]); ++ $this->formDataSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with('') ++ ->willReturn([]); ++ $this->productAttributeMock->expects($this->once()) ++ ->method('getId') ++ ->willReturn(1); ++ $this->productAttributeMock->expects($this->once()) ++ ->method('getAttributeCode') ++ ->willReturn('test_code'); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); +@@ -203,4 +266,74 @@ public function testExecute() + + $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); + } ++ ++ /** ++ * @return void ++ * @throws \Magento\Framework\Exception\NotFoundException ++ */ ++ public function testExecuteWithOptionsDataError() ++ { ++ $serializedOptions = '{"key":"value"}'; ++ $message = "The attribute couldn't be saved due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."; ++ ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['isAjax', null, true], ++ ['serialized_options', '[]', $serializedOptions], ++ ]); ++ $this->formDataSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willThrowException(new \InvalidArgumentException('Some exception')); ++ $this->messageManagerMock->expects($this->once()) ++ ->method('addErrorMessage') ++ ->with($message); ++ $this->addReturnResultConditions('catalog/*/edit', ['_current' => true], ['error' => true]); ++ ++ $this->getModel()->execute(); ++ } ++ ++ /** ++ * @param string $path ++ * @param array $params ++ * @param array $response ++ * @return void ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ private function addReturnResultConditions(string $path = '', array $params = [], array $response = []) ++ { ++ $layoutMock = $this->getMockBuilder(LayoutInterface::class) ++ ->setMethods(['initMessages', 'getMessagesBlock']) ++ ->getMockForAbstractClass(); ++ $this->layoutFactoryMock->expects($this->once()) ++ ->method('create') ++ ->with() ++ ->willReturn($layoutMock); ++ $layoutMock->expects($this->once()) ++ ->method('initMessages') ++ ->with(); ++ $messageBlockMock = $this->getMockBuilder(Messages::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $layoutMock->expects($this->once()) ++ ->method('getMessagesBlock') ++ ->willReturn($messageBlockMock); ++ $messageBlockMock->expects($this->once()) ++ ->method('getGroupedHtml') ++ ->willReturn('message1'); ++ $this->resultFactoryMock->expects($this->once()) ++ ->method('create') ++ ->with(ResultFactory::TYPE_JSON) ++ ->willReturn($this->redirectMock); ++ $response = array_merge($response, [ ++ 'messages' => ['message1'], ++ 'params' => $params, ++ ]); ++ $this->redirectMock->expects($this->once()) ++ ->method('setData') ++ ->with($response) ++ ->willReturnSelf(); ++ } + } +diff --git a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +index 9c747393cc72..750d38f60e13 100644 +--- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php ++++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +@@ -9,6 +9,7 @@ + use Magento\Catalog\Model\ResourceModel\Eav\Attribute; + use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; + use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; ++use Magento\Framework\Serialize\Serializer\FormData; + use Magento\Framework\Controller\Result\Json as ResultJson; + use Magento\Framework\Controller\Result\JsonFactory as ResultJsonFactory; + use Magento\Framework\Escaper; +@@ -61,6 +62,11 @@ class ValidateTest extends AttributeTest + */ + protected $layoutMock; + ++ /** ++ * @var FormData|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $formDataSerializer; ++ + protected function setUp() + { + parent::setUp(); +@@ -86,6 +92,9 @@ protected function setUp() + ->getMock(); + $this->layoutMock = $this->getMockBuilder(LayoutInterface::class) + ->getMockForAbstractClass(); ++ $this->formDataSerializer = $this->getMockBuilder(FormData::class) ++ ->disableOriginalConstructor() ++ ->getMock(); + + $this->contextMock->expects($this->any()) + ->method('getObjectManager') +@@ -100,25 +109,28 @@ protected function getModel() + return $this->objectManager->getObject( + Validate::class, + [ +- 'context' => $this->contextMock, +- 'attributeLabelCache' => $this->attributeLabelCacheMock, +- 'coreRegistry' => $this->coreRegistryMock, +- 'resultPageFactory' => $this->resultPageFactoryMock, +- 'resultJsonFactory' => $this->resultJsonFactoryMock, +- 'layoutFactory' => $this->layoutFactoryMock, +- 'multipleAttributeList' => ['select' => 'option'] ++ 'context' => $this->contextMock, ++ 'attributeLabelCache' => $this->attributeLabelCacheMock, ++ 'coreRegistry' => $this->coreRegistryMock, ++ 'resultPageFactory' => $this->resultPageFactoryMock, ++ 'resultJsonFactory' => $this->resultJsonFactoryMock, ++ 'layoutFactory' => $this->layoutFactoryMock, ++ 'multipleAttributeList' => ['select' => 'option'], ++ 'formDataSerializer' => $this->formDataSerializer, + ] + ); + } + + public function testExecute() + { ++ $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['frontend_label', null, 'test_frontend_label'], + ['attribute_code', null, 'test_attribute_code'], + ['new_attribute_set_name', null, 'test_attribute_set_name'], ++ ['serialized_options', '[]', $serializedOptions], + ]); + $this->objectManagerMock->expects($this->exactly(2)) + ->method('create') +@@ -160,6 +172,7 @@ public function testExecute() + */ + public function testUniqueValidation(array $options, $isError) + { ++ $serializedOptions = '{"key":"value"}'; + $countFunctionCalls = ($isError) ? 6 : 5; + $this->requestMock->expects($this->exactly($countFunctionCalls)) + ->method('getParam') +@@ -167,10 +180,15 @@ public function testUniqueValidation(array $options, $isError) + ['frontend_label', null, null], + ['attribute_code', null, "test_attribute_code"], + ['new_attribute_set_name', null, 'test_attribute_set_name'], +- ['option', null, $options], +- ['message_key', null, Validate::DEFAULT_MESSAGE_KEY] ++ ['message_key', null, Validate::DEFAULT_MESSAGE_KEY], ++ ['serialized_options', '[]', $serializedOptions], + ]); + ++ $this->formDataSerializer->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willReturn($options); ++ + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); +@@ -203,68 +221,84 @@ public function provideUniqueData() + return [ + 'no values' => [ + [ +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "", +- ] +- ], false ++ 'option' => [ ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "", ++ ], ++ ], ++ ++ ], ++ false, + ], + 'valid options' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [2, 0], +- "option_2" => [3, 0], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [2, 0], ++ "option_2" => [3, 0], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "", +- ] +- ], false ++ ], ++ false, + ], + 'duplicate options' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [1, 0], +- "option_2" => [3, 0], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [1, 0], ++ "option_2" => [3, 0], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "", +- ] +- ], true ++ ], ++ true, + ], + 'duplicate and deleted' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [1, 0], +- "option_2" => [3, 0], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [1, 0], ++ "option_2" => [3, 0], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "1", ++ "option_2" => "", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "1", +- "option_2" => "", +- ] +- ], false ++ ], ++ false, + ], + 'empty and deleted' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [2, 0], +- "option_2" => ["", ""], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [2, 0], ++ "option_2" => ["", ""], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "1", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "1", +- ] +- ], false ++ ], ++ false, + ], + ]; + } +@@ -278,6 +312,7 @@ public function provideUniqueData() + */ + public function testEmptyOption(array $options, $result) + { ++ $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ +@@ -285,10 +320,15 @@ public function testEmptyOption(array $options, $result) + ['frontend_input', 'select', 'multipleselect'], + ['attribute_code', null, "test_attribute_code"], + ['new_attribute_set_name', null, 'test_attribute_set_name'], +- ['option', null, $options], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ++ ['serialized_options', '[]', $serializedOptions], + ]); + ++ $this->formDataSerializer->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willReturn($options); ++ + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); +@@ -320,32 +360,38 @@ public function provideEmptyOption() + return [ + 'empty admin scope options' => [ + [ +- 'value' => [ +- "option_0" => [''], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [''], ++ ], + ], + ], + (object) [ + 'error' => true, + 'message' => 'The value of Admin scope can\'t be empty.', +- ] ++ ], + ], + 'not empty admin scope options' => [ + [ +- 'value' => [ +- "option_0" => ['asdads'], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => ['asdads'], ++ ], + ], + ], + (object) [ + 'error' => false, +- ] ++ ], + ], + 'empty admin scope options and deleted' => [ + [ +- 'value' => [ +- "option_0" => [''], +- ], +- 'delete' => [ +- 'option_0' => '1', ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [''], ++ ], ++ 'delete' => [ ++ 'option_0' => '1', ++ ], + ], + ], + (object) [ +@@ -354,11 +400,13 @@ public function provideEmptyOption() + ], + 'empty admin scope options and not deleted' => [ + [ +- 'value' => [ +- "option_0" => [''], +- ], +- 'delete' => [ +- 'option_0' => '0', ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [''], ++ ], ++ 'delete' => [ ++ 'option_0' => '0', ++ ], + ], + ], + (object) [ +@@ -368,4 +416,55 @@ public function provideEmptyOption() + ], + ]; + } ++ ++ /** ++ * @return void ++ * @throws \Magento\Framework\Exception\NotFoundException ++ */ ++ public function testExecuteWithOptionsDataError() ++ { ++ $serializedOptions = '{"key":"value"}'; ++ $message = "The attribute couldn't be validated due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."; ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['frontend_label', null, 'test_frontend_label'], ++ ['attribute_code', null, 'test_attribute_code'], ++ ['new_attribute_set_name', null, 'test_attribute_set_name'], ++ ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ++ ['serialized_options', '[]', $serializedOptions], ++ ]); ++ ++ $this->formDataSerializer->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willThrowException(new \InvalidArgumentException('Some exception')); ++ ++ $this->objectManagerMock->expects($this->once()) ++ ->method('create') ++ ->willReturnMap([ ++ [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], ++ [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] ++ ]); ++ ++ $this->attributeMock->expects($this->once()) ++ ->method('loadByCode') ++ ->willReturnSelf(); ++ $this->attributeSetMock->expects($this->never()) ++ ->method('setEntityTypeId') ++ ->willReturnSelf(); ++ $this->resultJsonFactoryMock->expects($this->once()) ++ ->method('create') ++ ->willReturn($this->resultJson); ++ $this->resultJson->expects($this->once()) ++ ->method('setJsonData') ++ ->with(json_encode([ ++ 'error' => true, ++ 'message' => $message, ++ ])) ++ ->willReturnSelf(); ++ ++ $this->getModel()->execute(); ++ } + } +diff --git a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +index b85b03852b62..3a0b2b4bf722 100644 +--- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php ++++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +@@ -9,8 +9,9 @@ + use Magento\Catalog\Controller\Adminhtml\Product\Attribute; + use Magento\Framework\App\RequestInterface; + use Magento\Framework\Cache\FrontendInterface; ++use Magento\Framework\Message\ManagerInterface; + use Magento\Framework\Registry; +-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; ++use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + use Magento\Framework\View\Result\PageFactory; + use Magento\Framework\Controller\ResultFactory; + +@@ -20,7 +21,7 @@ + class AttributeTest extends \PHPUnit\Framework\TestCase + { + /** +- * @var ObjectManager ++ * @var ObjectManagerHelper + */ + protected $objectManager; + +@@ -54,9 +55,14 @@ class AttributeTest extends \PHPUnit\Framework\TestCase + */ + protected $resultFactoryMock; + ++ /** ++ * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $messageManager; ++ + protected function setUp() + { +- $this->objectManager = new ObjectManager($this); ++ $this->objectManager = new ObjectManagerHelper($this); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); +@@ -74,6 +80,9 @@ protected function setUp() + $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->getMock(); ++ $this->messageManager = $this->getMockBuilder(ManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMock(); + + $this->contextMock->expects($this->any()) + ->method('getRequest') +@@ -81,6 +90,9 @@ protected function setUp() + $this->contextMock->expects($this->any()) + ->method('getResultFactory') + ->willReturn($this->resultFactoryMock); ++ $this->contextMock->expects($this->once()) ++ ->method('getMessageManager') ++ ->willReturn($this->messageManager); + } + + /** +diff --git a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +index 8a5f1919f78b..eeacc90fba91 100644 +--- a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml ++++ b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +@@ -40,13 +40,16 @@ function getFrontTab() { + + function checkOptionsPanelVisibility(){ + if($('manage-options-panel')){ +- var panel = $('manage-options-panel').up('.fieldset'); ++ var panel = $('manage-options-panel').up('.fieldset'), ++ activePanelClass = 'selected-type-options'; + + if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect')){ + panel.show(); ++ panel.addClass(activePanelClass); + } + else { + panel.hide(); ++ panel.removeClass(activePanelClass); + } + } + } +diff --git a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js +index 6ea005915763..7adc0dcfdf40 100644 +--- a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js ++++ b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js +@@ -20,7 +20,6 @@ define([ + + return function (config) { + var optionPanel = jQuery('#manage-options-panel'), +- optionsValues = [], + editForm = jQuery('#edit_form'), + attributeOption = { + table: $('attribute-options-table'), +@@ -145,7 +144,9 @@ define([ + + return optionDefaultInputType; + } +- }; ++ }, ++ tableBody = jQuery(), ++ activePanelClass = 'selected-type-options'; + + if ($('add_new_option_button')) { + Event.observe('add_new_option_button', 'click', attributeOption.add.bind(attributeOption, {}, true)); +@@ -180,30 +181,32 @@ define([ + }); + }); + } +- editForm.on('submit', function () { +- optionPanel.find('input') +- .each(function () { +- if (this.disabled) { +- return; ++ editForm.on('beforeSubmit', function () { ++ var optionContainer = optionPanel.find('table tbody'), ++ optionsValues; ++ ++ if (optionPanel.hasClass(activePanelClass)) { ++ optionsValues = jQuery.map( ++ optionContainer.find('tr'), ++ function (row) { ++ return jQuery(row).find('input, select, textarea').serialize(); + } +- +- if (this.type === 'checkbox' || this.type === 'radio') { +- if (this.checked) { +- optionsValues.push(this.name + '=' + jQuery(this).val()); +- } +- } else { +- optionsValues.push(this.name + '=' + jQuery(this).val()); +- } +- }); +- jQuery('') +- .attr({ +- type: 'hidden', +- name: 'serialized_options' +- }) +- .val(JSON.stringify(optionsValues)) +- .prependTo(editForm); +- optionPanel.find('table') +- .replaceWith(jQuery('
').text(jQuery.mage.__('Sending attribute values as package.'))); ++ ); ++ jQuery('') ++ .attr({ ++ type: 'hidden', ++ name: 'serialized_options' ++ }) ++ .val(JSON.stringify(optionsValues)) ++ .prependTo(editForm); ++ } ++ tableBody = optionContainer.detach(); ++ }); ++ editForm.on('afterValidate.error highlight.validate', function () { ++ if (optionPanel.hasClass(activePanelClass)) { ++ optionPanel.find('table').append(tableBody); ++ jQuery('input[name="serialized_options"]').remove(); ++ } + }); + window.attributeOption = attributeOption; + window.optionDefaultInputType = attributeOption.getOptionInputType(); +diff --git a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +index 383c97a166d3..72d27152d639 100644 +--- a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php ++++ b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +@@ -16,6 +16,8 @@ + class Save + { + /** ++ * Performs the conversion of the frontend input value. ++ * + * @param Attribute\Save $subject + * @param RequestInterface $request + * @return array +@@ -26,15 +28,6 @@ public function beforeDispatch(Attribute\Save $subject, RequestInterface $reques + $data = $request->getPostValue(); + + if (isset($data['frontend_input'])) { +- //Data is serialized to overcome issues caused by max_input_vars value if it's modification is unavailable. +- //See subject controller code and comments for more info. +- if (isset($data['serialized_swatch_values']) +- && in_array($data['frontend_input'], ['swatch_visual', 'swatch_text']) +- ) { +- $data['serialized_options'] = $data['serialized_swatch_values']; +- unset($data['serialized_swatch_values']); +- } +- + switch ($data['frontend_input']) { + case 'swatch_visual': + $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL; +diff --git a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js +index 1a58e4b6f2e7..6ef046ae1955 100644 +--- a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js ++++ b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js +@@ -16,7 +16,8 @@ define([ + 'use strict'; + + return function (optionConfig) { +- var swatchProductAttributes = { ++ var activePanelClass = 'selected-type-options', ++ swatchProductAttributes = { + frontendInput: $('#frontend_input'), + isFilterable: $('#is_filterable'), + isFilterableInSearch: $('#is_filterable_in_search'), +@@ -337,6 +338,7 @@ define([ + */ + _showPanel: function (el) { + el.closest('.fieldset').show(); ++ el.addClass(activePanelClass); + this._render(el.attr('id')); + }, + +@@ -346,6 +348,7 @@ define([ + */ + _hidePanel: function (el) { + el.closest('.fieldset').hide(); ++ el.removeClass(activePanelClass); + }, + + /** +@@ -413,7 +416,11 @@ define([ + }; + + $(function () { +- var editForm = $('#edit_form'); ++ var editForm = $('#edit_form'), ++ swatchVisualPanel = $('#swatch-visual-options-panel'), ++ swatchTextPanel = $('#swatch-text-options-panel'), ++ tableBody = $(), ++ activePanel = $(); + + $('#frontend_input').bind('change', function () { + swatchProductAttributes.bindAttributeInputType(); +@@ -429,30 +436,35 @@ define([ + .collapsable() + .collapse('hide'); + +- editForm.on('submit', function () { +- var activePanel, +- swatchValues = [], +- swatchVisualPanel = $('#swatch-visual-options-panel'), +- swatchTextPanel = $('#swatch-text-options-panel'); ++ editForm.on('beforeSubmit', function () { ++ var optionContainer, optionsValues; + +- activePanel = swatchTextPanel.is(':visible') ? swatchTextPanel : swatchVisualPanel; ++ activePanel = swatchTextPanel.hasClass(activePanelClass) ? swatchTextPanel : swatchVisualPanel; ++ optionContainer = activePanel.find('table tbody'); + +- activePanel.find('table input') +- .each(function () { +- swatchValues.push(this.name + '=' + $(this).val()); +- }); ++ if (activePanel.hasClass(activePanelClass)) { ++ optionsValues = $.map( ++ optionContainer.find('tr'), ++ function (row) { ++ return $(row).find('input, select, textarea').serialize(); ++ } ++ ); ++ $('') ++ .attr({ ++ type: 'hidden', ++ name: 'serialized_options' ++ }) ++ .val(JSON.stringify(optionsValues)) ++ .prependTo(editForm); ++ } + +- $('').attr({ +- type: 'hidden', +- name: 'serialized_swatch_values' +- }) +- .val(JSON.stringify(swatchValues)) +- .prependTo(editForm); +- +- [swatchVisualPanel, swatchTextPanel].forEach(function (el) { +- $(el).find('table') +- .replaceWith($('
').text($.mage.__('Sending swatch values as package.'))); +- }); ++ tableBody = optionContainer.detach(); ++ }); ++ editForm.on('afterValidate.error highlight.validate', function () { ++ if (activePanel.hasClass(activePanelClass)) { ++ activePanel.find('table').append(tableBody); ++ $('input[name="serialized_options"]').remove(); ++ } + }); + }); + +diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +index f1086f4871f3..2d9dea375e97 100644 +--- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php ++++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +@@ -91,15 +91,18 @@ public function persist(FixtureInterface $fixture = null) + $data['frontend_label'] = [0 => $data['frontend_label']]; + + if (isset($data['options'])) { ++ $optionsData = []; + foreach ($data['options'] as $key => $values) { ++ $optionRowData = []; + $index = 'option_' . $key; + if ($values['is_default'] == 'Yes') { +- $data['default'][] = $index; ++ $optionRowData['default'][] = $index; + } +- $data['option']['value'][$index] = [$values['admin'], $values['view']]; +- $data['option']['order'][$index] = $key; ++ $optionRowData['option']['value'][$index] = [$values['admin'], $values['view']]; ++ $optionRowData['option']['order'][$index] = $key; ++ $optionsData[] = $optionRowData; + } +- unset($data['options']); ++ $data['options'] = $optionsData; + } + + $data = $this->changeStructureOfTheData($data); +@@ -134,11 +137,39 @@ public function persist(FixtureInterface $fixture = null) + } + + /** ++ * Additional data handling. ++ * + * @param array $data + * @return array + */ +- protected function changeStructureOfTheData(array $data) ++ protected function changeStructureOfTheData(array $data): array + { ++ if (!isset($data['options'])) { ++ return $data; ++ } ++ ++ $serializedOptions = $this->getSerializeOptions($data['options']); ++ if ($serializedOptions) { ++ $data['serialized_options'] = $serializedOptions; ++ unset($data['options']); ++ } ++ + return $data; + } ++ ++ /** ++ * Provides serialized product attribute options. ++ * ++ * @param array $data ++ * @return string ++ */ ++ protected function getSerializeOptions(array $data): string ++ { ++ $options = []; ++ foreach ($data as $optionRowData) { ++ $options[] = http_build_query($optionRowData); ++ } ++ ++ return json_encode($options); ++ } + } +diff --git a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php +index 083fa246c96e..f4adb9dec125 100644 +--- a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php ++++ b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php +@@ -29,21 +29,32 @@ public function __construct(DataInterface $configuration, EventManagerInterface + ]; + } + ++ /** ++ * @inheritdoc ++ */ ++ protected function changeStructureOfTheData(array $data): array ++ { ++ return parent::changeStructureOfTheData($data); ++ } ++ + /** + * Re-map options from default options structure to swatches structure, + * as swatches was initially created with name convention differ from other attributes. + * +- * @param array $data +- * @return array ++ * @inheritdoc + */ +- protected function changeStructureOfTheData(array $data) ++ protected function getSerializeOptions(array $data): string + { +- $data = parent::changeStructureOfTheData($data); +- $data['optiontext'] = $data['option']; +- $data['swatchtext'] = [ +- 'value' => $data['option']['value'] +- ]; +- unset($data['option']); +- return $data; ++ $options = []; ++ foreach ($data as $optionRowData) { ++ $optionRowData['optiontext'] = $optionRowData['option']; ++ $optionRowData['swatchtext'] = [ ++ 'value' => $optionRowData['option']['value'] ++ ]; ++ unset($optionRowData['option']); ++ $options[] = http_build_query($optionRowData); ++ } ++ ++ return json_encode($options); + } + } +diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +index 098b18d6f38c..45c1583d7640 100644 +--- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php ++++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +@@ -282,13 +282,15 @@ public function testLargeOptionsDataSet() + $optionsData = []; + $expectedOptionsLabels = []; + for ($i = 0; $i < $optionsCount; $i++) { +- $order = $i + 1; +- $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; ++ $expectedOptionLabelOnStoreView = 'value_' . $i . '_store_1'; + $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; +- $optionsData []= "option[order][option_{$i}]={$order}"; +- $optionsData []= "option[value][option_{$i}][0]=value_{$i}_admin"; +- $optionsData []= "option[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; +- $optionsData []= "option[delete][option_{$i}="; ++ $optionId = 'option_' . $i; ++ $optionRowData = []; ++ $optionRowData['option']['order'][$optionId] = $i + 1; ++ $optionRowData['option']['value'][$optionId][0] = 'value_' . $i . '_admin'; ++ $optionRowData['option']['value'][$optionId][1] = $expectedOptionLabelOnStoreView; ++ $optionRowData['option']['delete'][$optionId] = ''; ++ $optionsData[] = http_build_query($optionRowData); + } + $attributeData['serialized_options'] = json_encode($optionsData); + $this->getRequest()->setPostValue($attributeData); +diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php +index be9fd96d7589..493c4dcadc1e 100644 +--- a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php ++++ b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php +@@ -7,6 +7,8 @@ + + namespace Magento\Swatches\Controller\Adminhtml\Product; + ++use Magento\Framework\App\Request\Http as HttpRequest; ++use Magento\Framework\Data\Form\FormKey; + use Magento\Framework\Exception\LocalizedException; + + /** +@@ -17,6 +19,21 @@ + */ + class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController + { ++ /** ++ * @var FormKey ++ */ ++ private $formKey; ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function setUp() ++ { ++ parent::setUp(); ++ ++ $this->formKey = $this->_objectManager->get(FormKey::class); ++ } ++ + /** + * Generate random hex color. + * +@@ -38,22 +55,27 @@ private function getSwatchVisualDataSet(int $optionsCount): array + $optionsData = []; + $expectedOptionsLabels = []; + for ($i = 0; $i < $optionsCount; $i++) { +- $order = $i + 1; +- $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; ++ $expectedOptionLabelOnStoreView = 'value_' . $i .'_store_1'; + $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; +- $optionsData []= "optionvisual[order][option_{$i}]={$order}"; +- $optionsData []= "defaultvisual[]=option_{$i}"; +- $optionsData []= "swatchvisual[value][option_{$i}]={$this->getRandomColor()}"; +- $optionsData []= "optionvisual[value][option_{$i}][0]=value_{$i}_admin"; +- $optionsData []= "optionvisual[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; +- $optionsData []= "optionvisual[delete][option_{$i}]="; ++ $optionId = 'option_' .$i; ++ $optionRowData = []; ++ $optionRowData['optionvisual']['order'][$optionId] = $i + 1; ++ $optionRowData['defaultvisual'][] = $optionId; ++ $optionRowData['swatchvisual']['value'][$optionId] = $this->getRandomColor(); ++ $optionRowData['optionvisual']['value'][$optionId][0] = 'value_' . $i .'_admin'; ++ $optionRowData['optionvisual']['value'][$optionId][1] = $expectedOptionLabelOnStoreView; ++ $optionRowData['optionvisual']['delete'][$optionId] = ''; ++ $optionsData[] = http_build_query($optionRowData); + } +- $optionsData []= "visual_swatch_validation="; +- $optionsData []= "visual_swatch_validation_unique="; ++ + return [ + 'attribute_data' => array_merge_recursive( + [ +- 'serialized_swatch_values' => json_encode($optionsData), ++ 'serialized_options' => json_encode($optionsData), ++ ], ++ [ ++ 'visual_swatch_validation' => '', ++ 'visual_swatch_validation_unique' => '', + ], + $this->getAttributePreset(), + [ +@@ -76,22 +98,27 @@ private function getSwatchTextDataSet(int $optionsCount): array + $optionsData = []; + $expectedOptionsLabels = []; + for ($i = 0; $i < $optionsCount; $i++) { +- $order = $i + 1; +- $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; ++ $expectedOptionLabelOnStoreView = 'value_' . $i . '_store_1'; + $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; +- $optionsData []= "optiontext[order][option_{$i}]={$order}"; +- $optionsData []= "defaulttext[]=option_{$i}"; +- $optionsData []= "swatchtext[value][option_{$i}]=x{$i}"; +- $optionsData []= "optiontext[value][option_{$i}][0]=value_{$i}_admin"; +- $optionsData []= "optiontext[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; +- $optionsData []= "optiontext[delete][option_{$i}]="; ++ $optionId = 'option_' . $i; ++ $optionRowData = []; ++ $optionRowData['optiontext']['order'][$optionId] = $i + 1; ++ $optionRowData['defaulttext'][] = $optionId; ++ $optionRowData['swatchtext']['value'][$optionId] = 'x' . $i ; ++ $optionRowData['optiontext']['value'][$optionId][0] = 'value_' . $i . '_admin'; ++ $optionRowData['optiontext']['value'][$optionId][1]= $expectedOptionLabelOnStoreView; ++ $optionRowData['optiontext']['delete'][$optionId]=''; ++ $optionsData[] = http_build_query($optionRowData); + } +- $optionsData []= "text_swatch_validation="; +- $optionsData []= "text_swatch_validation_unique="; ++ + return [ + 'attribute_data' => array_merge_recursive( + [ +- 'serialized_swatch_values' => json_encode($optionsData), ++ 'serialized_options' => json_encode($optionsData), ++ ], ++ [ ++ 'text_swatch_validation' => '', ++ 'text_swatch_validation_unique' => '', + ], + $this->getAttributePreset(), + [ +@@ -111,7 +138,6 @@ private function getSwatchTextDataSet(int $optionsCount): array + private function getAttributePreset(): array + { + return [ +- 'serialized_options' => '[]', + 'form_key' => 'XxtpPYjm2YPYUlAt', + 'frontend_label' => [ + 0 => 'asdasd', +@@ -176,7 +202,9 @@ public function testLargeOptionsDataSet( + int $expectedOptionsCount, + array $expectedLabels + ) { ++ $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($attributeData); ++ $this->getRequest()->setPostValue('form_key', $this->formKey->getFormKey()); + $this->dispatch('backend/catalog/product_attribute/save'); + $entityTypeId = $this->_objectManager->create( + \Magento\Eav\Model\Entity::class +diff --git a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor/magento/framework/Serialize/Serializer/FormData.php +new file mode 100644 +index 000000000000..077e91797d2b +--- /dev/null ++++ b/vendor/magento/framework/Serialize/Serializer/FormData.php +@@ -0,0 +1,55 @@ ++serializer = $serializer; ++ } ++ ++ /** ++ * Provides form data from the serialized data. ++ * ++ * @param string $serializedData ++ * @return array ++ * @throws \InvalidArgumentException ++ */ ++ public function unserialize(string $serializedData): array ++ { ++ $encodedFields = $this->serializer->unserialize($serializedData); ++ ++ if (!is_array($encodedFields)) { ++ throw new \InvalidArgumentException('Unable to unserialize value.'); ++ } ++ ++ $formData = []; ++ foreach ($encodedFields as $item) { ++ $decodedFieldData = []; ++ parse_str($item, $decodedFieldData); ++ $formData = array_replace_recursive($formData, $decodedFieldData); ++ } ++ ++ return $formData; ++ } ++} +diff --git a/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php +new file mode 100644 +index 000000000000..7729a2da97ef +--- /dev/null ++++ b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php +@@ -0,0 +1,106 @@ ++jsonSerializerMock = $this->createMock(Json::class); ++ $this->formDataSerializer = new FormData($this->jsonSerializerMock); ++ } ++ ++ /** ++ * @param string $serializedData ++ * @param array $encodedFields ++ * @param array $expectedFormData ++ * @return void ++ * @dataProvider unserializeDataProvider ++ */ ++ public function testUnserialize(string $serializedData, array $encodedFields, array $expectedFormData) ++ { ++ $this->jsonSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedData) ++ ->willReturn($encodedFields); ++ ++ $this->assertEquals($expectedFormData, $this->formDataSerializer->unserialize($serializedData)); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function unserializeDataProvider(): array ++ { ++ return [ ++ [ ++ 'serializedData' => ++ '["option[order][option_0]=1","option[value][option_0]=1","option[delete][option_0]="]', ++ 'encodedFields' => [ ++ 'option[order][option_0]=1', ++ 'option[value][option_0]=1', ++ 'option[delete][option_0]=', ++ ], ++ 'expectedFormData' => [ ++ 'option' => [ ++ 'order' => [ ++ 'option_0' => '1', ++ ], ++ 'value' => [ ++ 'option_0' => '1', ++ ], ++ 'delete' => [ ++ 'option_0' => '', ++ ], ++ ], ++ ], ++ ], ++ [ ++ 'serializedData' => '[]', ++ 'encodedFields' => [], ++ 'expectedFormData' => [], ++ ], ++ ]; ++ } ++ ++ /** ++ * @return void ++ * @expectedException InvalidArgumentException ++ * @expectedExceptionMessage Unable to unserialize value. ++ */ ++ public function testUnserializeWithWrongSerializedData() ++ { ++ $serializedData = 'test'; ++ ++ $this->jsonSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedData) ++ ->willReturn('test'); ++ ++ $this->formDataSerializer->unserialize($serializedData); ++ } ++} +diff --git a/lib/web/mage/backend/validation.js b/lib/web/mage/backend/validation.js +index d3ab7dd086a4..f141fb3eeb8d 100644 +--- a/lib/web/mage/backend/validation.js ++++ b/lib/web/mage/backend/validation.js +@@ -171,6 +171,7 @@ + this._submit(); + } else { + this._showErrors(response); ++ $(this.element[0]).trigger('afterValidate.error'); + $('body').trigger('processStop'); + } + }, diff --git a/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch new file mode 100644 index 0000000..06e9137 --- /dev/null +++ b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch @@ -0,0 +1,337 @@ +Index: vendor/magento/module-authorizenet/Model/Directpost.php +<+>UTF-8 +=================================================================== +--- vendor/magento/module-authorizenet/Model/Directpost.php (revision 01fbfeba9bd743266199e30260376a9c1b95frevision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +@@ -543,15 +543,16 @@ + public function validateResponse() + { + $response = $this->getResponse(); +- //md5 check +- if (!$this->getConfigData('trans_md5') +- || !$this->getConfigData('login') +- || !$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login')) ++ $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; ++ ++ //hash check ++ if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) + ) { + throw new \Magento\Framework\Exception\LocalizedException( + __('The transaction was declined because the response hash validation failed.') + ); + } ++ + return true; + } + +Index: vendor/magento/module-authorizenet/Model/Directpost/Request.php +<+>UTF-8 +=================================================================== +--- vendor/magento/module-authorizenet/Model/Directpost/Request.php (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) ++++ vendor/magento/module-authorizenet/Model/Directpost/Request.php (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +@@ -7,6 +7,7 @@ + namespace Magento\Authorizenet\Model\Directpost; + + use Magento\Authorizenet\Model\Request as AuthorizenetRequest; ++use Magento\Framework\Intl\DateTimeFactory; + + /** + * Authorize.net request model for DirectPost model +@@ -18,9 +19,33 @@ + */ + protected $_transKey = null; + ++ /** ++ * Hexadecimal signature key. ++ * ++ * @var string ++ */ ++ private $signatureKey = ''; ++ ++ /** ++ * @var DateTimeFactory ++ */ ++ private $dateTimeFactory; ++ ++ /** ++ * @param DateTimeFactory $dateTimeFactory ++ * @param array $data ++ */ ++ public function __construct( ++ DateTimeFactory $dateTimeFactory, ++ array $data = [] ++ ) { ++ $this->dateTimeFactory = $dateTimeFactory; ++ parent::__construct($data); ++ } ++ + /** + * Return merchant transaction key. +- * Needed to generate sign. ++ * Needed to generate MD5 sign. + * + * @return string + */ +@@ -31,7 +56,7 @@ + + /** + * Set merchant transaction key. +- * Needed to generate sign. ++ * Needed to generate MD5 sign. + * + * @param string $transKey + * @return $this +@@ -43,7 +68,7 @@ + } + + /** +- * Generates the fingerprint for request. ++ * Generates the MD5 fingerprint for request. + * + * @param string $merchantApiLoginId + * @param string $merchantTransactionKey +@@ -63,7 +88,7 @@ + ) { + return hash_hmac( + "md5", +- $merchantApiLoginId . "^" . $fpSequence . "^" . $fpTimestamp . "^" . $amount . "^" . $currencyCode, ++ $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode, + $merchantTransactionKey + ); + } +@@ -85,6 +110,7 @@ + ->setXRelayUrl($paymentMethod->getRelayUrl()); + + $this->_setTransactionKey($paymentMethod->getConfigData('trans_key')); ++ $this->setSignatureKey($paymentMethod->getConfigData('signature_key')); + return $this; + } + +@@ -168,17 +194,81 @@ + */ + public function signRequestData() + { +- $fpTimestamp = time(); +- $hash = $this->generateRequestSign( +- $this->getXLogin(), +- $this->_getTransactionKey(), +- $this->getXAmount(), +- $this->getXCurrencyCode(), +- $this->getXFpSequence(), +- $fpTimestamp +- ); ++ $fpDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); ++ $fpTimestamp = $fpDate->getTimestamp(); ++ ++ if (!empty($this->getSignatureKey())) { ++ $hash = $this->generateSha2RequestSign( ++ $this->getXLogin(), ++ $this->getSignatureKey(), ++ $this->getXAmount(), ++ $this->getXCurrencyCode(), ++ $this->getXFpSequence(), ++ $fpTimestamp ++ ); ++ } else { ++ $hash = $this->generateRequestSign( ++ $this->getXLogin(), ++ $this->_getTransactionKey(), ++ $this->getXAmount(), ++ $this->getXCurrencyCode(), ++ $this->getXFpSequence(), ++ $fpTimestamp ++ ); ++ } ++ + $this->setXFpTimestamp($fpTimestamp); + $this->setXFpHash($hash); ++ + return $this; + } ++ ++ /** ++ * Generates the SHA2 fingerprint for request. ++ * ++ * @param string $merchantApiLoginId ++ * @param string $merchantSignatureKey ++ * @param string $amount ++ * @param string $currencyCode ++ * @param string $fpSequence An invoice number or random number. ++ * @param string $fpTimestamp ++ * @return string The fingerprint. ++ */ ++ private function generateSha2RequestSign( ++ $merchantApiLoginId, ++ $merchantSignatureKey, ++ $amount, ++ $currencyCode, ++ $fpSequence, ++ $fpTimestamp ++ ): string { ++ $message = $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode; ++ ++ return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantSignatureKey))); ++ } ++ ++ /** ++ * Return merchant hexadecimal signature key. ++ * ++ * Needed to generate SHA2 sign. ++ * ++ * @return string ++ */ ++ private function getSignatureKey(): string ++ { ++ return $this->signatureKey; ++ } ++ ++ /** ++ * Set merchant hexadecimal signature key. ++ * ++ * Needed to generate SHA2 sign. ++ * ++ * @param string $signatureKey ++ * @return void ++ */ ++ private function setSignatureKey(string $signatureKey) ++ { ++ $this->signatureKey = $signatureKey; ++ } + } +Index: vendor/magento/module-authorizenet/Model/Directpost/Response.php +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-authorizenet/Model/Directpost/Response.php (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) ++++ vendor/magento/module-authorizenet/Model/Directpost/Response.php (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +@@ -24,27 +24,33 @@ + */ + public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) + { +- if (!$amount) { +- $amount = '0.00'; +- } +- + return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); + } + + /** + * Return if is valid order id. + * +- * @param string $merchantMd5 ++ * @param string $storedHash + * @param string $merchantApiLogin + * @return bool + */ +- public function isValidHash($merchantMd5, $merchantApiLogin) ++ public function isValidHash($storedHash, $merchantApiLogin) + { +- $hash = $this->generateHash($merchantMd5, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); ++ if (empty($this->getData('x_amount'))) { ++ $this->setData('x_amount', '0.00'); ++ } + +- return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); +- } ++ if (!empty($this->getData('x_SHA2_Hash'))) { ++ $hash = $this->generateSha2Hash($storedHash); ++ return Security::compareStrings($hash, $this->getData('x_SHA2_Hash')); ++ } elseif (!empty($this->getData('x_MD5_Hash'))) { ++ $hash = $this->generateHash($storedHash, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); ++ return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); ++ } + ++ return false; ++ } ++ + /** + * Return if this is approved response from Authorize.net auth request. + * +@@ -54,4 +60,54 @@ + { + return $this->getXResponseCode() == \Magento\Authorizenet\Model\Directpost::RESPONSE_CODE_APPROVED; + } ++ ++ /** ++ * Generates an SHA2 hash to compare against AuthNet's. ++ * ++ * @param string $signatureKey ++ * @return string ++ * @see https://support.authorize.net/s/article/MD5-Hash-End-of-Life-Signature-Key-Replacement ++ */ ++ private function generateSha2Hash(string $signatureKey): string ++ { ++ $hashFields = [ ++ 'x_trans_id', ++ 'x_test_request', ++ 'x_response_code', ++ 'x_auth_code', ++ 'x_cvv2_resp_code', ++ 'x_cavv_response', ++ 'x_avs_code', ++ 'x_method', ++ 'x_account_number', ++ 'x_amount', ++ 'x_company', ++ 'x_first_name', ++ 'x_last_name', ++ 'x_address', ++ 'x_city', ++ 'x_state', ++ 'x_zip', ++ 'x_country', ++ 'x_phone', ++ 'x_fax', ++ 'x_email', ++ 'x_ship_to_company', ++ 'x_ship_to_first_name', ++ 'x_ship_to_last_name', ++ 'x_ship_to_address', ++ 'x_ship_to_city', ++ 'x_ship_to_state', ++ 'x_ship_to_zip', ++ 'x_ship_to_country', ++ 'x_invoice_num', ++ ]; ++ ++ $message = '^'; ++ foreach ($hashFields as $field) { ++ $message .= ($this->getData($field) ?? '') . '^'; ++ } ++ ++ return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); ++ } + } +Index: vendor/magento/module-authorizenet/etc/adminhtml/system.xml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-authorizenet/etc/adminhtml/system.xml (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) ++++ vendor/magento/module-authorizenet/etc/adminhtml/system.xml (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +@@ -29,6 +29,10 @@ + + Magento\Config\Model\Config\Backend\Encrypted + ++ ++ ++ Magento\Config\Model\Config\Backend\Encrypted ++ + + + Magento\Config\Model\Config\Backend\Encrypted +Index: vendor/magento/module-authorizenet/etc/config.xml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-authorizenet/etc/config.xml (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) ++++ vendor/magento/module-authorizenet/etc/config.xml (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +@@ -22,6 +22,7 @@ + Credit Card Direct Post (Authorize.net) + + ++ + 0 + USD + 1 diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch new file mode 100644 index 0000000..47644db --- /dev/null +++ b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch @@ -0,0 +1,934 @@ +diff --git a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +new file mode 100644 +index 00000000000..c9899663487 +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,102 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Check if amqpProperties['application_headers'] have 'store_id' and use it to setCurrentStore ++ * Restore original store value in consumer process after execution. ++ * Reject queue messages because of wrong store_id. ++ * ++ * @param SubjectMassConsumerEnvelopeCallback $subject ++ * @param callable $proceed ++ * @param EnvelopeInterface $message ++ * @return void ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function aroundExecute( ++ SubjectMassConsumerEnvelopeCallback $subject, ++ callable $proceed, ++ EnvelopeInterface $message ++ ): void { ++ $amqpProperties = $message->getProperties(); ++ if (isset($amqpProperties['application_headers'])) { ++ $headers = $amqpProperties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ $headers = $headers->getNativeData(); ++ } ++ if (isset($headers['store_id'])) { ++ $storeId = $headers['store_id']; ++ try { ++ $currentStoreId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $this->logger->error( ++ sprintf( ++ "Can't set currentStoreId during processing queue. Message rejected. Error %s.", ++ $e->getMessage() ++ ) ++ ); ++ $subject->getQueue()->reject($message, false, $e->getMessage()); ++ ++ return; ++ } ++ if ($storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($storeId); ++ } ++ } ++ } ++ $proceed($message); ++ if (isset($storeId, $currentStoreId) && $storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($currentStoreId);//restore original store value ++ } ++ } ++} +diff --git a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +new file mode 100644 +index 00000000000..9c54c8485db +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +@@ -0,0 +1,115 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Set current store_id in amqpProperties['application_headers'] ++ * so consumer may check store_id and execute operation in correct store scope. ++ * Prevent publishing inconsistent messages because of store_id not defined or wrong. ++ * ++ * @param SubjectExchange $subject ++ * @param string $topic ++ * @param EnvelopeInterface[] $envelopes ++ * @return array ++ * @throws AMQPInvalidArgumentException ++ * @throws \LogicException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeEnqueue(SubjectExchange $subject, $topic, array $envelopes): array ++ { ++ try { ++ $storeId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $errorMessage = sprintf( ++ "Can't get current storeId and inject to amqp message. Error %s.", ++ $e->getMessage() ++ ); ++ $this->logger->error($errorMessage); ++ throw new \LogicException($errorMessage); ++ } ++ ++ $updatedEnvelopes = []; ++ foreach ($envelopes as $envelope) { ++ $properties = $envelope->getProperties(); ++ if (empty($properties)) { ++ $properties = []; ++ } ++ if (isset($properties['application_headers'])) { ++ $headers = $properties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ try { ++ $headers->set('store_id', $storeId); ++ // phpcs:ignore Magento2.Exceptions.ThrowCatch ++ } catch (AMQPInvalidArgumentException $ea) { ++ $errorMessage = sprintf("Can't set storeId to amqp message. Error %s.", $ea->getMessage()); ++ $this->logger->error($errorMessage); ++ throw new AMQPInvalidArgumentException($errorMessage); ++ } ++ } ++ } else { ++ $properties['application_headers'] = new AMQPTable(['store_id' => $storeId]); ++ } ++ $updatedEnvelopes[] = $this->envelopeFactory->create( ++ [ ++ 'body' => $envelope->getBody(), ++ 'properties' => $properties, ++ ] ++ ); ++ } ++ if (!empty($updatedEnvelopes)) { ++ $envelopes = $updatedEnvelopes; ++ } ++ ++ return [$topic, $envelopes]; ++ } ++} +diff --git a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json +index b50e951b46f..54ebfde3b68 100644 +--- a/vendor/magento/module-amqp/composer.json ++++ b/vendor/magento/module-amqp/composer.json +@@ -8,6 +11,10 @@ + "magento/framework": "102.0.*", + "magento/framework-amqp": "100.3.*", + "magento/framework-message-queue": "100.3.*", ++ "magento/module-store": "101.0.*", + "php": "~7.1.3||~7.2.0" + }, ++ "suggest": { ++ "magento/module-asynchronous-operations": "*", ++ }, + "type": "magento2-module", + "license": [ + "OSL-3.0", +diff --git a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml +index 920bb72261e..8bcf290f4a9 100644 +--- a/vendor/magento/module-amqp/etc/di.xml ++++ b/vendor/magento/module-amqp/etc/di.xml +@@ -72,4 +72,10 @@ + \Magento\Framework\Amqp\Bulk\Exchange + + ++ ++ ++ ++ ++ ++ + +diff --git a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml +index 1768a9b121c..5f49dae67f9 100644 +--- a/vendor/magento/module-amqp/etc/module.xml ++++ b/vendor/magento/module-amqp/etc/module.xml +@@ -6,5 +6,9 @@ + */ + --> + +- ++ ++ ++ ++ ++ + +diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +index af1ef4400e4..618b16f549e 100644 +--- a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +@@ -8,17 +8,10 @@ declare(strict_types=1); + + namespace Magento\AsynchronousOperations\Model; + +-use Magento\Framework\App\ResourceConnection; +-use Psr\Log\LoggerInterface; +-use Magento\Framework\MessageQueue\MessageLockException; +-use Magento\Framework\MessageQueue\ConnectionLostException; +-use Magento\Framework\Exception\NotFoundException; + use Magento\Framework\MessageQueue\CallbackInvoker; + use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; + use Magento\Framework\MessageQueue\EnvelopeInterface; + use Magento\Framework\MessageQueue\QueueInterface; +-use Magento\Framework\MessageQueue\LockInterface; +-use Magento\Framework\MessageQueue\MessageController; + use Magento\Framework\MessageQueue\ConsumerInterface; + + /** +@@ -33,57 +27,31 @@ class MassConsumer implements ConsumerInterface + */ + private $invoker; + +- /** +- * @var \Magento\Framework\App\ResourceConnection +- */ +- private $resource; +- + /** + * @var \Magento\Framework\MessageQueue\ConsumerConfigurationInterface + */ + private $configuration; +- +- /** +- * @var \Magento\Framework\MessageQueue\MessageController +- */ +- private $messageController; +- +- /** +- * @var LoggerInterface +- */ +- private $logger; + + /** +- * @var OperationProcessor ++ * @var MassConsumerEnvelopeCallbackFactory + */ +- private $operationProcessor; ++ private $massConsumerEnvelopeCallback; + + /** + * Initialize dependencies. + * + * @param CallbackInvoker $invoker +- * @param ResourceConnection $resource +- * @param MessageController $messageController + * @param ConsumerConfigurationInterface $configuration +- * @param OperationProcessorFactory $operationProcessorFactory +- * @param LoggerInterface $logger ++ * @param MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback + */ + public function __construct( + CallbackInvoker $invoker, +- ResourceConnection $resource, +- MessageController $messageController, + ConsumerConfigurationInterface $configuration, +- OperationProcessorFactory $operationProcessorFactory, +- LoggerInterface $logger ++ MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback + ) { + $this->invoker = $invoker; +- $this->resource = $resource; +- $this->messageController = $messageController; + $this->configuration = $configuration; +- $this->operationProcessor = $operationProcessorFactory->create([ +- 'configuration' => $configuration +- ]); +- $this->logger = $logger; ++ $this->massConsumerEnvelopeCallback = $massConsumerEnvelopeCallback; + } + + /** +@@ -108,38 +84,15 @@ class MassConsumer implements ConsumerInterface + */ + private function getTransactionCallback(QueueInterface $queue) + { +- return function (EnvelopeInterface $message) use ($queue) { +- /** @var LockInterface $lock */ +- $lock = null; +- try { +- $topicName = $message->getProperties()['topic_name']; +- $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); ++ $callbackInstance = $this->massConsumerEnvelopeCallback->create( ++ [ ++ 'configuration' => $this->configuration, ++ 'queue' => $queue, ++ ] ++ ); + +- $allowedTopics = $this->configuration->getTopicNames(); +- if (in_array($topicName, $allowedTopics)) { +- $this->operationProcessor->process($message->getBody()); +- } else { +- $queue->reject($message); +- return; +- } +- $queue->acknowledge($message); +- } catch (MessageLockException $exception) { +- $queue->acknowledge($message); +- } catch (ConnectionLostException $e) { +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } catch (NotFoundException $e) { +- $queue->acknowledge($message); +- $this->logger->warning($e->getMessage()); +- } catch (\Exception $e) { +- $queue->reject($message, false, $e->getMessage()); +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } ++ return function (EnvelopeInterface $message) use ($callbackInstance) { ++ $callbackInstance->execute($message); + }; + } + } +diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +new file mode 100644 +index 00000000000..1327fcc2672 +--- /dev/null ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,138 @@ ++resource = $resource; ++ $this->messageController = $messageController; ++ $this->configuration = $configuration; ++ $this->operationProcessor = $operationProcessorFactory->create( ++ [ ++ 'configuration' => $configuration, ++ ] ++ ); ++ $this->logger = $logger; ++ $this->queue = $queue; ++ } ++ ++ /** ++ * Get transaction callback. This handles the case of async. ++ * ++ * @param EnvelopeInterface $message ++ * @return void ++ */ ++ public function execute(EnvelopeInterface $message): void ++ { ++ $queue = $this->queue; ++ /** @var LockInterface $lock */ ++ $lock = null; ++ try { ++ $topicName = $message->getProperties()['topic_name']; ++ $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); ++ ++ $allowedTopics = $this->configuration->getTopicNames(); ++ if (in_array($topicName, $allowedTopics)) { ++ $this->operationProcessor->process($message->getBody()); ++ } else { ++ $queue->reject($message); ++ ++ return; ++ } ++ $queue->acknowledge($message); ++ } catch (MessageLockException $exception) { ++ $queue->acknowledge($message); ++ } catch (ConnectionLostException $e) { ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } catch (NotFoundException $e) { ++ $queue->acknowledge($message); ++ $this->logger->warning($e->getMessage()); ++ } catch (\Exception $e) { ++ $queue->reject($message, false, $e->getMessage()); ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } ++ } ++ ++ /** ++ * Get message queue. ++ * ++ * @return QueueInterface ++ */ ++ public function getQueue(): QueueInterface ++ { ++ return $this->queue; ++ } ++} +diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +new file mode 100644 +index 00000000000..61ec73990c9 +--- /dev/null ++++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +@@ -0,0 +1,374 @@ ++objectManager = Bootstrap::getObjectManager(); ++ $this->logFilePath = TESTS_TEMP_DIR . "/MessageQueueTestLog.txt"; ++ $this->registry = $this->objectManager->get(Registry::class); ++ ++ $params = array_merge_recursive( ++ Bootstrap::getInstance()->getAppInitParams(), ++ ['MAGE_DIRS' => ['cache' => ['path' => TESTS_TEMP_DIR . '/cache']]] ++ ); ++ ++ /** @var PublisherConsumerController publisherConsumerController */ ++ $this->publisherConsumerController = $this->objectManager->create( ++ PublisherConsumerController::class, ++ [ ++ 'consumers' => $this->consumers, ++ 'logFilePath' => $this->logFilePath, ++ 'appInitParams' => $params, ++ ] ++ ); ++ $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); ++ ++ try { ++ $this->publisherConsumerController->initialize(); ++ } catch (EnvironmentPreconditionException $e) { ++ $this->markTestSkipped($e->getMessage()); ++ } catch (PreconditionFailedException $e) { ++ $this->fail( ++ $e->getMessage() ++ ); ++ } ++ ++ parent::setUp(); ++ } ++ ++ /** ++ * @param string $storeCode ++ * @return void ++ * ++ * @dataProvider storeProvider ++ * @magentoApiDataFixture Magento/Store/_files/core_fixturestore.php ++ */ ++ public function testAsyncScheduleBulkMultistore(string $storeCode): void ++ { ++ $product = $this->getProductData(); ++ $this->_markTestAsRestOnly(); ++ ++ /** @var Store $store */ ++ $store = $this->objectManager->create(Store::class); ++ $store->load(self::STORE_CODE_FROM_FIXTURE); ++ $this->assertEquals( ++ self::STORE_NAME_FROM_FIXTURE, ++ $store->getName(), ++ 'Precondition failed: fixture store was not created.' ++ ); ++ ++ try { ++ /** @var Product $productModel */ ++ $productModel = $this->objectManager->create( ++ Product::class, ++ ['data' => $product['product']] ++ ); ++ $this->productRepository->save($productModel); ++ } catch (\Exception $e) { ++ $this->fail("Precondition failed: product was not created."); ++ } ++ ++ $this->asyncScheduleAndTest($product, $storeCode); ++ $this->clearProducts(); ++ } ++ ++ /** ++ * @param array $product ++ * @param string|null $storeCode ++ */ ++ private function asyncScheduleAndTest(array $product, string $storeCode = null): void ++ { ++ $sku = $product['product'][Product::SKU]; ++ $productName = $product['product'][Product::NAME]; ++ $newProductName = $product['product'][Product::NAME] . $storeCode; ++ ++ $this->skus[] = $sku; ++ ++ $product['product'][Product::NAME] = $newProductName; ++ $product['product'][Product::TYPE_ID] = 'virtual'; ++ ++ $response = $this->updateProductAsync($product, $sku, $storeCode); ++ ++ $this->assertArrayHasKey(self::BULK_UUID_KEY, $response); ++ $this->assertNotNull($response[self::BULK_UUID_KEY]); ++ ++ $this->assertCount(1, $response['request_items']); ++ $this->assertEquals('accepted', $response['request_items'][0]['status']); ++ $this->assertFalse($response['errors']); ++ ++ //assert product is updated ++ try { ++ $this->publisherConsumerController->waitForAsynchronousResult( ++ [$this, 'assertProductCreation'], ++ [$product] ++ ); ++ } catch (PreconditionFailedException $e) { ++ $this->fail("Product is not updated"); ++ } ++ ++ $requestData = ['id' => $sku, 'sku' => $sku]; ++ ++ foreach ($this->stores as $checkingStore) { ++ $serviceInfo = [ ++ 'rest' => [ ++ 'resourcePath' => self::REST_RESOURCE_PATH . '/' . $sku, ++ 'httpMethod' => Request::HTTP_METHOD_GET, ++ ] ++ ]; ++ $storeResponse = $this->_webApiCall($serviceInfo, $requestData, null, $checkingStore); ++ if ($checkingStore == $storeCode || $storeCode == self::STORE_CODE_ALL) { ++ $this->assertEquals( ++ $newProductName, ++ $storeResponse[Product::NAME], ++ sprintf( ++ 'Product name in %s store is invalid after updating in store %s.', ++ $checkingStore, ++ $storeCode ++ ) ++ ); ++ } else { ++ $this->assertEquals( ++ $productName, ++ $storeResponse[Product::NAME], ++ sprintf( ++ 'Product name in %s store is invalid after updating in store %s.', ++ $checkingStore, ++ $storeCode ++ ) ++ ); ++ } ++ } ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function tearDown() ++ { ++ $this->clearProducts(); ++ $this->publisherConsumerController->stopConsumers(); ++ parent::tearDown(); ++ } ++ ++ /** ++ * @throws Exception ++ */ ++ private function clearProducts() ++ { ++ /** @var Collection $productCollection */ ++ $productCollection = $this->objectManager->create(Collection::class) ++ ->addAttributeToFilter('sku', ['in' => $this->skus]) ++ ->load(); ++ ++ if ($productCollection->getSize() == 0) { ++ return; ++ } ++ ++ $this->registry->unregister('isSecureArea'); ++ $this->registry->register('isSecureArea', true); ++ try { ++ foreach ($this->skus as $sku) { ++ $this->productRepository->deleteById($sku); ++ } ++ // phpcs:ignore Magento2.Exceptions.ThrowCatch ++ } catch (\Exception $e) { ++ //nothing to delete ++ throw $e; ++ } ++ $this->registry->unregister('isSecureArea'); ++ ++ $size = $productCollection->clear()->getSize(); ++ ++ if ($size > 0) { ++ //phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new Exception(new Phrase("Collection size after clearing the products: %size", ['size' => $size])); ++ } ++ $this->skus = []; ++ } ++ ++ /** ++ * @return array ++ */ ++ public function getProductData(): array ++ { ++ $productBuilder = function ($data) { ++ return array_replace_recursive( ++ $this->getSimpleProductData(), ++ $data ++ ); ++ }; ++ ++ return [ ++ 'product' => ++ $productBuilder( ++ [ ++ ProductInterface::TYPE_ID => 'simple', ++ ProductInterface::SKU => 'multistore-sku-test-1', ++ ProductInterface::NAME => 'Test Name ', ++ ] ++ ), ++ ]; ++ } ++ ++ /** ++ * @return array ++ */ ++ public function storeProvider(): array ++ { ++ $dataSets = []; ++ foreach ($this->stores as $store) { ++ $dataSets[$store] = [$store]; ++ } ++ ++ return $dataSets; ++ } ++ ++ /** ++ * Get Simple Product Data ++ * ++ * @param array $productData ++ * @return array ++ */ ++ private function getSimpleProductData($productData = []): array ++ { ++ return [ ++ ProductInterface::SKU => isset($productData[ProductInterface::SKU]) ++ ? $productData[ProductInterface::SKU] : uniqid('sku-', true), ++ ProductInterface::NAME => isset($productData[ProductInterface::NAME]) ++ ? $productData[ProductInterface::NAME] : uniqid('sku-', true), ++ ProductInterface::VISIBILITY => 4, ++ ProductInterface::TYPE_ID => 'simple', ++ ProductInterface::PRICE => 3.62, ++ ProductInterface::STATUS => 1, ++ ProductInterface::ATTRIBUTE_SET_ID => 4, ++ ]; ++ } ++ ++ /** ++ * @param array $requestData ++ * @param string $sku ++ * @param string|null $storeCode ++ * @return mixed ++ */ ++ private function updateProductAsync(array $requestData, string $sku, string $storeCode = null) ++ { ++ $serviceInfo = [ ++ 'rest' => [ ++ 'resourcePath' => self::ASYNC_RESOURCE_PATH . '/' . $sku, ++ 'httpMethod' => Request::HTTP_METHOD_PUT, ++ ], ++ ]; ++ ++ return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode); ++ } ++ ++ /** ++ * @param array $product ++ * @return bool ++ */ ++ public function assertProductCreation(array $product): bool ++ { ++ $sku = $product['product'][Product::SKU]; ++ $collection = $this->objectManager->create(Collection::class) ++ ->addAttributeToFilter(Product::SKU, ['eq' => $sku]) ++ ->addAttributeToFilter(Product::TYPE_ID, ['eq' => 'virtual']) ++ ->load(); ++ $size = $collection->getSize(); ++ ++ return $size > 0; ++ } ++} +-- +2.17.1 diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch new file mode 100644 index 0000000..911fcf6 --- /dev/null +++ b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch @@ -0,0 +1,948 @@ +diff --git a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +new file mode 100644 +index 00000000000..c9899663487 +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,102 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Check if amqpProperties['application_headers'] have 'store_id' and use it to setCurrentStore ++ * Restore original store value in consumer process after execution. ++ * Reject queue messages because of wrong store_id. ++ * ++ * @param SubjectMassConsumerEnvelopeCallback $subject ++ * @param callable $proceed ++ * @param EnvelopeInterface $message ++ * @return void ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function aroundExecute( ++ SubjectMassConsumerEnvelopeCallback $subject, ++ callable $proceed, ++ EnvelopeInterface $message ++ ): void { ++ $amqpProperties = $message->getProperties(); ++ if (isset($amqpProperties['application_headers'])) { ++ $headers = $amqpProperties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ $headers = $headers->getNativeData(); ++ } ++ if (isset($headers['store_id'])) { ++ $storeId = $headers['store_id']; ++ try { ++ $currentStoreId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $this->logger->error( ++ sprintf( ++ "Can't set currentStoreId during processing queue. Message rejected. Error %s.", ++ $e->getMessage() ++ ) ++ ); ++ $subject->getQueue()->reject($message, false, $e->getMessage()); ++ ++ return; ++ } ++ if ($storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($storeId); ++ } ++ } ++ } ++ $proceed($message); ++ if (isset($storeId, $currentStoreId) && $storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($currentStoreId);//restore original store value ++ } ++ } ++} +diff --git a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +new file mode 100644 +index 00000000000..9c54c8485db +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +@@ -0,0 +1,115 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Set current store_id in amqpProperties['application_headers'] ++ * so consumer may check store_id and execute operation in correct store scope. ++ * Prevent publishing inconsistent messages because of store_id not defined or wrong. ++ * ++ * @param SubjectExchange $subject ++ * @param string $topic ++ * @param EnvelopeInterface[] $envelopes ++ * @return array ++ * @throws AMQPInvalidArgumentException ++ * @throws \LogicException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeEnqueue(SubjectExchange $subject, $topic, array $envelopes): array ++ { ++ try { ++ $storeId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $errorMessage = sprintf( ++ "Can't get current storeId and inject to amqp message. Error %s.", ++ $e->getMessage() ++ ); ++ $this->logger->error($errorMessage); ++ throw new \LogicException($errorMessage); ++ } ++ ++ $updatedEnvelopes = []; ++ foreach ($envelopes as $envelope) { ++ $properties = $envelope->getProperties(); ++ if (empty($properties)) { ++ $properties = []; ++ } ++ if (isset($properties['application_headers'])) { ++ $headers = $properties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ try { ++ $headers->set('store_id', $storeId); ++ // phpcs:ignore Magento2.Exceptions.ThrowCatch ++ } catch (AMQPInvalidArgumentException $ea) { ++ $errorMessage = sprintf("Can't set storeId to amqp message. Error %s.", $ea->getMessage()); ++ $this->logger->error($errorMessage); ++ throw new AMQPInvalidArgumentException($errorMessage); ++ } ++ } ++ } else { ++ $properties['application_headers'] = new AMQPTable(['store_id' => $storeId]); ++ } ++ $updatedEnvelopes[] = $this->envelopeFactory->create( ++ [ ++ 'body' => $envelope->getBody(), ++ 'properties' => $properties, ++ ] ++ ); ++ } ++ if (!empty($updatedEnvelopes)) { ++ $envelopes = $updatedEnvelopes; ++ } ++ ++ return [$topic, $envelopes]; ++ } ++} +diff --git a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json +index b50e951b46f..54ebfde3b68 100644 +--- a/vendor/magento/module-amqp/composer.json ++++ b/vendor/magento/module-amqp/composer.json +@@ -8,6 +11,10 @@ + "magento/framework": "102.0.*", + "magento/framework-amqp": "100.3.*", + "magento/framework-message-queue": "100.3.*", ++ "magento/module-store": "101.0.*", + "php": "~7.1.3||~7.2.0" + }, ++ "suggest": { ++ "magento/module-asynchronous-operations": "*", ++ }, + "type": "magento2-module", + "license": [ + "OSL-3.0", +diff --git a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml +index 920bb72261e..8bcf290f4a9 100644 +--- a/vendor/magento/module-amqp/etc/di.xml ++++ b/vendor/magento/module-amqp/etc/di.xml +@@ -72,4 +72,10 @@ + \Magento\Framework\Amqp\Bulk\Exchange + + ++ ++ ++ ++ ++ ++ + +diff --git a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml +index 1768a9b121c..5f49dae67f9 100644 +--- a/vendor/magento/module-amqp/etc/module.xml ++++ b/vendor/magento/module-amqp/etc/module.xml +@@ -6,5 +6,9 @@ + */ + --> + +- ++ ++ ++ ++ ++ + +diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +index af1ef4400e4..618b16f549e 100644 +--- a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +@@ -8,18 +8,11 @@ declare(strict_types=1); + + namespace Magento\AsynchronousOperations\Model; + +-use Magento\Framework\App\ResourceConnection; + use Magento\Framework\Registry; +-use Psr\Log\LoggerInterface; +-use Magento\Framework\MessageQueue\MessageLockException; +-use Magento\Framework\MessageQueue\ConnectionLostException; +-use Magento\Framework\Exception\NotFoundException; + use Magento\Framework\MessageQueue\CallbackInvokerInterface; + use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; + use Magento\Framework\MessageQueue\EnvelopeInterface; + use Magento\Framework\MessageQueue\QueueInterface; +-use Magento\Framework\MessageQueue\LockInterface; +-use Magento\Framework\MessageQueue\MessageController; + use Magento\Framework\MessageQueue\ConsumerInterface; + + /** +@@ -34,66 +27,39 @@ class MassConsumer implements ConsumerInterface + */ + private $invoker; + +- /** +- * @var \Magento\Framework\App\ResourceConnection +- */ +- private $resource; +- + /** + * @var \Magento\Framework\MessageQueue\ConsumerConfigurationInterface + */ + private $configuration; + + /** +- * @var \Magento\Framework\MessageQueue\MessageController +- */ +- private $messageController; +- +- /** +- * @var LoggerInterface +- */ +- private $logger; +- +- /** +- * @var OperationProcessor ++ * @var Registry + */ +- private $operationProcessor; ++ private $registry; + + /** +- * @var Registry ++ * @var MassConsumerEnvelopeCallbackFactory + */ +- private $registry; ++ private $massConsumerEnvelopeCallback; + + /** + * Initialize dependencies. + * + * @param CallbackInvokerInterface $invoker +- * @param ResourceConnection $resource +- * @param MessageController $messageController + * @param ConsumerConfigurationInterface $configuration +- * @param OperationProcessorFactory $operationProcessorFactory +- * @param LoggerInterface $logger ++ * @param MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback + * @param Registry $registry + */ + public function __construct( + CallbackInvokerInterface $invoker, +- ResourceConnection $resource, +- MessageController $messageController, + ConsumerConfigurationInterface $configuration, +- OperationProcessorFactory $operationProcessorFactory, +- LoggerInterface $logger, ++ MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback, + Registry $registry = null + ) { + $this->invoker = $invoker; +- $this->resource = $resource; +- $this->messageController = $messageController; + $this->configuration = $configuration; +- $this->operationProcessor = $operationProcessorFactory->create([ +- 'configuration' => $configuration +- ]); +- $this->logger = $logger; +- $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance() +- ->get(Registry::class); ++ $this->massConsumerEnvelopeCallback = $massConsumerEnvelopeCallback; ++ $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Registry::class); + } + + /** +@@ -122,38 +88,15 @@ class MassConsumer implements ConsumerInterface + */ + private function getTransactionCallback(QueueInterface $queue) + { +- return function (EnvelopeInterface $message) use ($queue) { +- /** @var LockInterface $lock */ +- $lock = null; +- try { +- $topicName = $message->getProperties()['topic_name']; +- $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); +- +- $allowedTopics = $this->configuration->getTopicNames(); +- if (in_array($topicName, $allowedTopics)) { +- $this->operationProcessor->process($message->getBody()); +- } else { +- $queue->reject($message); +- return; +- } +- $queue->acknowledge($message); +- } catch (MessageLockException $exception) { +- $queue->acknowledge($message); +- } catch (ConnectionLostException $e) { +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } catch (NotFoundException $e) { +- $queue->acknowledge($message); +- $this->logger->warning($e->getMessage()); +- } catch (\Exception $e) { +- $queue->reject($message, false, $e->getMessage()); +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } ++ $callbackInstance = $this->massConsumerEnvelopeCallback->create( ++ [ ++ 'configuration' => $this->configuration, ++ 'queue' => $queue, ++ ] ++ ); ++ ++ return function (EnvelopeInterface $message) use ($callbackInstance) { ++ $callbackInstance->execute($message); + }; + } + } +diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +new file mode 100644 +index 00000000000..1327fcc2672 +--- /dev/null ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,138 @@ ++resource = $resource; ++ $this->messageController = $messageController; ++ $this->configuration = $configuration; ++ $this->operationProcessor = $operationProcessorFactory->create( ++ [ ++ 'configuration' => $configuration, ++ ] ++ ); ++ $this->logger = $logger; ++ $this->queue = $queue; ++ } ++ ++ /** ++ * Get transaction callback. This handles the case of async. ++ * ++ * @param EnvelopeInterface $message ++ * @return void ++ */ ++ public function execute(EnvelopeInterface $message): void ++ { ++ $queue = $this->queue; ++ /** @var LockInterface $lock */ ++ $lock = null; ++ try { ++ $topicName = $message->getProperties()['topic_name']; ++ $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); ++ ++ $allowedTopics = $this->configuration->getTopicNames(); ++ if (in_array($topicName, $allowedTopics)) { ++ $this->operationProcessor->process($message->getBody()); ++ } else { ++ $queue->reject($message); ++ ++ return; ++ } ++ $queue->acknowledge($message); ++ } catch (MessageLockException $exception) { ++ $queue->acknowledge($message); ++ } catch (ConnectionLostException $e) { ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } catch (NotFoundException $e) { ++ $queue->acknowledge($message); ++ $this->logger->warning($e->getMessage()); ++ } catch (\Exception $e) { ++ $queue->reject($message, false, $e->getMessage()); ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } ++ } ++ ++ /** ++ * Get message queue. ++ * ++ * @return QueueInterface ++ */ ++ public function getQueue(): QueueInterface ++ { ++ return $this->queue; ++ } ++} +diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +new file mode 100644 +index 00000000000..61ec73990c9 +--- /dev/null ++++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +@@ -0,0 +1,374 @@ ++objectManager = Bootstrap::getObjectManager(); ++ $this->logFilePath = TESTS_TEMP_DIR . "/MessageQueueTestLog.txt"; ++ $this->registry = $this->objectManager->get(Registry::class); ++ ++ $params = array_merge_recursive( ++ Bootstrap::getInstance()->getAppInitParams(), ++ ['MAGE_DIRS' => ['cache' => ['path' => TESTS_TEMP_DIR . '/cache']]] ++ ); ++ ++ /** @var PublisherConsumerController publisherConsumerController */ ++ $this->publisherConsumerController = $this->objectManager->create( ++ PublisherConsumerController::class, ++ [ ++ 'consumers' => $this->consumers, ++ 'logFilePath' => $this->logFilePath, ++ 'appInitParams' => $params, ++ ] ++ ); ++ $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); ++ ++ try { ++ $this->publisherConsumerController->initialize(); ++ } catch (EnvironmentPreconditionException $e) { ++ $this->markTestSkipped($e->getMessage()); ++ } catch (PreconditionFailedException $e) { ++ $this->fail( ++ $e->getMessage() ++ ); ++ } ++ ++ parent::setUp(); ++ } ++ ++ /** ++ * @param string $storeCode ++ * @return void ++ * ++ * @dataProvider storeProvider ++ * @magentoApiDataFixture Magento/Store/_files/core_fixturestore.php ++ */ ++ public function testAsyncScheduleBulkMultistore(string $storeCode): void ++ { ++ $product = $this->getProductData(); ++ $this->_markTestAsRestOnly(); ++ ++ /** @var Store $store */ ++ $store = $this->objectManager->create(Store::class); ++ $store->load(self::STORE_CODE_FROM_FIXTURE); ++ $this->assertEquals( ++ self::STORE_NAME_FROM_FIXTURE, ++ $store->getName(), ++ 'Precondition failed: fixture store was not created.' ++ ); ++ ++ try { ++ /** @var Product $productModel */ ++ $productModel = $this->objectManager->create( ++ Product::class, ++ ['data' => $product['product']] ++ ); ++ $this->productRepository->save($productModel); ++ } catch (\Exception $e) { ++ $this->fail("Precondition failed: product was not created."); ++ } ++ ++ $this->asyncScheduleAndTest($product, $storeCode); ++ $this->clearProducts(); ++ } ++ ++ /** ++ * @param array $product ++ * @param string|null $storeCode ++ */ ++ private function asyncScheduleAndTest(array $product, string $storeCode = null): void ++ { ++ $sku = $product['product'][Product::SKU]; ++ $productName = $product['product'][Product::NAME]; ++ $newProductName = $product['product'][Product::NAME] . $storeCode; ++ ++ $this->skus[] = $sku; ++ ++ $product['product'][Product::NAME] = $newProductName; ++ $product['product'][Product::TYPE_ID] = 'virtual'; ++ ++ $response = $this->updateProductAsync($product, $sku, $storeCode); ++ ++ $this->assertArrayHasKey(self::BULK_UUID_KEY, $response); ++ $this->assertNotNull($response[self::BULK_UUID_KEY]); ++ ++ $this->assertCount(1, $response['request_items']); ++ $this->assertEquals('accepted', $response['request_items'][0]['status']); ++ $this->assertFalse($response['errors']); ++ ++ //assert product is updated ++ try { ++ $this->publisherConsumerController->waitForAsynchronousResult( ++ [$this, 'assertProductCreation'], ++ [$product] ++ ); ++ } catch (PreconditionFailedException $e) { ++ $this->fail("Product is not updated"); ++ } ++ ++ $requestData = ['id' => $sku, 'sku' => $sku]; ++ ++ foreach ($this->stores as $checkingStore) { ++ $serviceInfo = [ ++ 'rest' => [ ++ 'resourcePath' => self::REST_RESOURCE_PATH . '/' . $sku, ++ 'httpMethod' => Request::HTTP_METHOD_GET, ++ ] ++ ]; ++ $storeResponse = $this->_webApiCall($serviceInfo, $requestData, null, $checkingStore); ++ if ($checkingStore == $storeCode || $storeCode == self::STORE_CODE_ALL) { ++ $this->assertEquals( ++ $newProductName, ++ $storeResponse[Product::NAME], ++ sprintf( ++ 'Product name in %s store is invalid after updating in store %s.', ++ $checkingStore, ++ $storeCode ++ ) ++ ); ++ } else { ++ $this->assertEquals( ++ $productName, ++ $storeResponse[Product::NAME], ++ sprintf( ++ 'Product name in %s store is invalid after updating in store %s.', ++ $checkingStore, ++ $storeCode ++ ) ++ ); ++ } ++ } ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function tearDown() ++ { ++ $this->clearProducts(); ++ $this->publisherConsumerController->stopConsumers(); ++ parent::tearDown(); ++ } ++ ++ /** ++ * @throws Exception ++ */ ++ private function clearProducts() ++ { ++ /** @var Collection $productCollection */ ++ $productCollection = $this->objectManager->create(Collection::class) ++ ->addAttributeToFilter('sku', ['in' => $this->skus]) ++ ->load(); ++ ++ if ($productCollection->getSize() == 0) { ++ return; ++ } ++ ++ $this->registry->unregister('isSecureArea'); ++ $this->registry->register('isSecureArea', true); ++ try { ++ foreach ($this->skus as $sku) { ++ $this->productRepository->deleteById($sku); ++ } ++ // phpcs:ignore Magento2.Exceptions.ThrowCatch ++ } catch (\Exception $e) { ++ //nothing to delete ++ throw $e; ++ } ++ $this->registry->unregister('isSecureArea'); ++ ++ $size = $productCollection->clear()->getSize(); ++ ++ if ($size > 0) { ++ //phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new Exception(new Phrase("Collection size after clearing the products: %size", ['size' => $size])); ++ } ++ $this->skus = []; ++ } ++ ++ /** ++ * @return array ++ */ ++ public function getProductData(): array ++ { ++ $productBuilder = function ($data) { ++ return array_replace_recursive( ++ $this->getSimpleProductData(), ++ $data ++ ); ++ }; ++ ++ return [ ++ 'product' => ++ $productBuilder( ++ [ ++ ProductInterface::TYPE_ID => 'simple', ++ ProductInterface::SKU => 'multistore-sku-test-1', ++ ProductInterface::NAME => 'Test Name ', ++ ] ++ ), ++ ]; ++ } ++ ++ /** ++ * @return array ++ */ ++ public function storeProvider(): array ++ { ++ $dataSets = []; ++ foreach ($this->stores as $store) { ++ $dataSets[$store] = [$store]; ++ } ++ ++ return $dataSets; ++ } ++ ++ /** ++ * Get Simple Product Data ++ * ++ * @param array $productData ++ * @return array ++ */ ++ private function getSimpleProductData($productData = []): array ++ { ++ return [ ++ ProductInterface::SKU => isset($productData[ProductInterface::SKU]) ++ ? $productData[ProductInterface::SKU] : uniqid('sku-', true), ++ ProductInterface::NAME => isset($productData[ProductInterface::NAME]) ++ ? $productData[ProductInterface::NAME] : uniqid('sku-', true), ++ ProductInterface::VISIBILITY => 4, ++ ProductInterface::TYPE_ID => 'simple', ++ ProductInterface::PRICE => 3.62, ++ ProductInterface::STATUS => 1, ++ ProductInterface::ATTRIBUTE_SET_ID => 4, ++ ]; ++ } ++ ++ /** ++ * @param array $requestData ++ * @param string $sku ++ * @param string|null $storeCode ++ * @return mixed ++ */ ++ private function updateProductAsync(array $requestData, string $sku, string $storeCode = null) ++ { ++ $serviceInfo = [ ++ 'rest' => [ ++ 'resourcePath' => self::ASYNC_RESOURCE_PATH . '/' . $sku, ++ 'httpMethod' => Request::HTTP_METHOD_PUT, ++ ], ++ ]; ++ ++ return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode); ++ } ++ ++ /** ++ * @param array $product ++ * @return bool ++ */ ++ public function assertProductCreation(array $product): bool ++ { ++ $sku = $product['product'][Product::SKU]; ++ $collection = $this->objectManager->create(Collection::class) ++ ->addAttributeToFilter(Product::SKU, ['eq' => $sku]) ++ ->addAttributeToFilter(Product::TYPE_ID, ['eq' => 'virtual']) ++ ->load(); ++ $size = $collection->getSize(); ++ ++ return $size > 0; ++ } ++} +-- +2.17.1 diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch b/patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch new file mode 100644 index 0000000..0f77f18 --- /dev/null +++ b/patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch @@ -0,0 +1,16 @@ +Index: vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/framework/DB/Adapter/Pdo/Mysql.php (revision 6a8701ca9402697f5eaf022e35b9217d3281546c) ++++ vendor/magento/framework/DB/Adapter/Pdo/Mysql.php (date 1553502112000) +@@ -2796,7 +2796,7 @@ + if (isset($condition['to'])) { + $query .= empty($query) ? '' : ' AND '; + $to = $this->_prepareSqlDateCondition($condition, 'to'); +- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); ++ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); + } + } elseif (array_key_exists($key, $conditionKeyMap)) { + $value = $condition[$key]; diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch b/patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch new file mode 100644 index 0000000..6ad7304 --- /dev/null +++ b/patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch @@ -0,0 +1,96 @@ +diff --git a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +index 7a1926c..331c667 100644 +--- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php ++++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +@@ -138,7 +138,9 @@ class Synchronizer + $productIds = []; + + foreach ($actions as $action) { +- $productIds[] = $action['product_id']; ++ if (isset($action['product_id']) && is_int($action['product_id'])) { ++ $productIds[] = $action['product_id']; ++ } + } + + return $productIds; +@@ -159,33 +161,37 @@ class Synchronizer + $customerId = $this->session->getCustomerId(); + $visitorId = $this->visitor->getId(); + $collection = $this->getActionsByType($typeId); +- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); +- +- /** +- * Note that collection is also filtered by visitor id and customer id +- * This collection shouldnt be flushed when visitor has products and then login +- * It can remove only products for visitor, or only products for customer +- * +- * ['product_id' => 'added_at'] +- * @var ProductFrontendActionInterface $item +- */ +- foreach ($collection as $item) { +- $this->entityManager->delete($item); +- } +- +- foreach ($productsData as $productId => $productData) { +- /** @var ProductFrontendActionInterface $action */ +- $action = $this->productFrontendActionFactory->create([ +- 'data' => [ +- 'visitor_id' => $customerId ? null : $visitorId, +- 'customer_id' => $this->session->getCustomerId(), +- 'added_at' => $productData['added_at'], +- 'product_id' => $productId, +- 'type_id' => $typeId +- ] +- ]); +- +- $this->entityManager->save($action); ++ $productIds = $this->getProductIdsByActions($productsData); ++ ++ if ($productIds) { ++ $collection->addFieldToFilter('product_id', $productIds); ++ ++ /** ++ * Note that collection is also filtered by visitor id and customer id ++ * This collection shouldn't be flushed when visitor has products and then login ++ * It can remove only products for visitor, or only products for customer ++ * ++ * ['product_id' => 'added_at'] ++ * @var ProductFrontendActionInterface $item ++ */ ++ foreach ($collection as $item) { ++ $this->entityManager->delete($item); ++ } ++ ++ foreach ($productsData as $productId => $productData) { ++ /** @var ProductFrontendActionInterface $action */ ++ $action = $this->productFrontendActionFactory->create([ ++ 'data' => [ ++ 'visitor_id' => $customerId ? null : $visitorId, ++ 'customer_id' => $this->session->getCustomerId(), ++ 'added_at' => $productData['added_at'], ++ 'product_id' => $productId, ++ 'type_id' => $typeId ++ ] ++ ]); ++ ++ $this->entityManager->save($action); ++ } + } + } + +diff --git a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +index 1449d6d..38085a3 100644 +--- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +@@ -2904,7 +2904,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + if (isset($condition['to'])) { + $query .= empty($query) ? '' : ' AND '; + $to = $this->_prepareSqlDateCondition($condition, 'to'); +- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); ++ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); + } + } elseif (array_key_exists($key, $conditionKeyMap)) { + $value = $condition[$key]; +-- +2.7.4 + diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch b/patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch new file mode 100644 index 0000000..e778277 --- /dev/null +++ b/patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch @@ -0,0 +1,312 @@ +diff --git a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +index 3ec8e96..72f6628 100644 +--- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php ++++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +@@ -16,6 +16,8 @@ use Magento\Customer\Model\Visitor; + use Magento\Framework\EntityManager\EntityManager; + + /** ++ * A Product Widget Synchronizer. ++ * + * Service which allows to sync product widget information, such as product id with db. In order to reuse this info + * on different devices + */ +@@ -85,9 +87,10 @@ class Synchronizer + } + + /** +- * Find lifetime in configuration. Configuration is hold in Stores Configuration +- * Also this configuration is generated by: +- * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration ++ * Finds lifetime in configuration. ++ * ++ * Configuration is hold in Stores Configuration. Also this configuration is generated by ++ * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} + * + * @param string $namespace + * @return int +@@ -108,6 +111,8 @@ class Synchronizer + } + + /** ++ * Filters actions. ++ * + * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that + * can be persisted in database. + * +@@ -138,7 +143,9 @@ class Synchronizer + $productIds = []; + + foreach ($actions as $action) { +- $productIds[] = $action['product_id']; ++ if (isset($action['product_id']) && is_int($action['product_id'])) { ++ $productIds[] = $action['product_id']; ++ } + } + + return $productIds; +@@ -159,33 +166,37 @@ class Synchronizer + $customerId = $this->session->getCustomerId(); + $visitorId = $this->visitor->getId(); + $collection = $this->getActionsByType($typeId); +- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); +- +- /** +- * Note that collection is also filtered by visitor id and customer id +- * This collection shouldn't be flushed when visitor has products and then login +- * It can remove only products for visitor, or only products for customer +- * +- * ['product_id' => 'added_at'] +- * @var ProductFrontendActionInterface $item +- */ +- foreach ($collection as $item) { +- $this->entityManager->delete($item); +- } +- +- foreach ($productsData as $productId => $productData) { +- /** @var ProductFrontendActionInterface $action */ +- $action = $this->productFrontendActionFactory->create([ +- 'data' => [ +- 'visitor_id' => $customerId ? null : $visitorId, +- 'customer_id' => $this->session->getCustomerId(), +- 'added_at' => $productData['added_at'], +- 'product_id' => $productId, +- 'type_id' => $typeId +- ] +- ]); +- +- $this->entityManager->save($action); ++ $productIds = $this->getProductIdsByActions($productsData); ++ ++ if ($productIds) { ++ $collection->addFieldToFilter('product_id', $productIds); ++ ++ /** ++ * Note that collection is also filtered by visitor id and customer id ++ * This collection shouldn't be flushed when visitor has products and then login ++ * It can remove only products for visitor, or only products for customer ++ * ++ * ['product_id' => 'added_at'] ++ * @var ProductFrontendActionInterface $item ++ */ ++ foreach ($collection as $item) { ++ $this->entityManager->delete($item); ++ } ++ ++ foreach ($productsData as $productId => $productData) { ++ /** @var ProductFrontendActionInterface $action */ ++ $action = $this->productFrontendActionFactory->create([ ++ 'data' => [ ++ 'visitor_id' => $customerId ? null : $visitorId, ++ 'customer_id' => $this->session->getCustomerId(), ++ 'added_at' => $productData['added_at'], ++ 'product_id' => $productId, ++ 'type_id' => $typeId ++ ] ++ ]); ++ ++ $this->entityManager->save($action); ++ } + } + } + +diff --git a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +index 441da10..69505d4 100644 +--- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +@@ -515,6 +515,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Special handling for PDO query(). ++ * + * All bind parameter names must begin with ':'. + * + * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. +@@ -595,6 +596,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Special handling for PDO query(). ++ * + * All bind parameter names must begin with ':'. + * + * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. +@@ -615,8 +617,9 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + } + + /** +- * Allows multiple queries -- to safeguard against SQL injection, USE CAUTION and verify that input +- * cannot be tampered with. ++ * Allows multiple queries -- to safeguard against SQL injection, ++ * ++ * USE CAUTION and verify that input cannot be tampered with. + * + * Special handling for PDO query(). + * All bind parameter names must begin with ':'. +@@ -1278,6 +1281,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Modify tables, used for upgrade process ++ * + * Change columns definitions, reset foreign keys, change tables comments and engines. + * + * The value of each array element is an associative array +@@ -1469,11 +1473,11 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + * + * Method revrited for handle empty arrays in value param + * +- * @param string $text The text with a placeholder. +- * @param mixed $value The value to quote. +- * @param string $type OPTIONAL SQL datatype ++ * @param string $text The text with a placeholder. ++ * @param mixed $value The value to quote. ++ * @param string $type OPTIONAL SQL datatype + * @param integer $count OPTIONAL count of placeholders to replace +- * @return string An SQL-safe quoted value placed into the orignal text. ++ * @return string An SQL-safe quoted value placed into the original text. + */ + public function quoteInto($text, $value, $type = null, $count = null) + { +@@ -1514,6 +1518,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Load DDL data from cache ++ * + * Return false if cache does not exists + * + * @param string $tableCacheKey the table cache key +@@ -1568,7 +1573,8 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Reset cached DDL data from cache +- * if table name is null - reset all cached DDL data ++ * ++ * If table name is null - reset all cached DDL data + * + * @param string $tableName + * @param string $schemaName OPTIONAL +@@ -1605,6 +1611,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Disallow DDL caching ++ * + * @return $this + */ + public function disallowDdlCache() +@@ -1615,6 +1622,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Allow DDL caching ++ * + * @return $this + */ + public function allowDdlCache() +@@ -1675,6 +1683,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Format described column to definition, ready to be added to ddl table. ++ * + * Return array with keys: name, type, length, options, comment + * + * @param array $columnData +@@ -1892,6 +1901,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Inserts a table row with specified data ++ * + * Special for Zero values to identity column + * + * @param string $table +@@ -2782,6 +2792,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Add new Foreign Key to table ++ * + * If Foreign Key with same name is exist - it will be deleted + * + * @param string $fkName +@@ -2954,7 +2965,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + if (isset($condition['to'])) { + $query .= empty($query) ? '' : ' AND '; + $to = $this->_prepareSqlDateCondition($condition, 'to'); +- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); ++ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); + } + } elseif (array_key_exists($key, $conditionKeyMap)) { + $value = $condition[$key]; +@@ -3016,6 +3027,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Prepare value for save in column ++ * + * Return converted to column data type value + * + * @param array $column the column describe array +@@ -3140,7 +3152,8 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Generate fragment of SQL, that check value against multiple condition cases +- * and return different result depends on them ++ * ++ * And return different result depends on them + * + * @param string $valueName Name of value to check + * @param array $casesResults Cases and results +@@ -3163,6 +3176,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Generate fragment of SQL, that combine together (concatenate) the results from data array ++ * + * All arguments in data must be quoted + * + * @param string[] $data +@@ -3177,6 +3191,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Generate fragment of SQL that returns length of character string ++ * + * The string argument must be quoted + * + * @param string $string +@@ -3371,6 +3386,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Retrieve valid index name ++ * + * Check index name length and allowed symbols + * + * @param string $tableName +@@ -3400,6 +3416,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Retrieve valid foreign key name ++ * + * Check foreign key name length and allowed symbols + * + * @param string $priTableName +@@ -3668,6 +3685,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Adds order by random to select object ++ * + * Possible using integer field for optimization + * + * @param Select $select +@@ -3845,6 +3863,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Parse text size ++ * + * Returns max allowed size if value great it + * + * @param string|int $size +@@ -3879,6 +3898,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + + /** + * Converts fetched blob into raw binary PHP data. ++ * + * The MySQL drivers do it nice, no processing required. + * + * @param mixed $value +-- +2.7.4 + diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch new file mode 100644 index 0000000..6b7cf93 --- /dev/null +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch @@ -0,0 +1,216 @@ +Index: vendor/magento/module-sales/Helper/Admin.php +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-sales/Helper/Admin.php (revision d46f8efde7d6e7c40859e833f462c30edc101476) ++++ vendor/magento/module-sales/Helper/Admin.php (revision 102b397919b1c82aa13118abb5345ad81c626370) +@@ -6,6 +6,8 @@ + + namespace Magento\Sales\Helper; + ++use Magento\Framework\App\ObjectManager; ++ + /** + * Sales admin helper. + */ +@@ -31,24 +33,33 @@ + */ + protected $escaper; + ++ /** ++ * @var \DOMDocumentFactory ++ */ ++ private $domDocumentFactory; ++ + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Sales\Model\Config $salesConfig + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Framework\Escaper $escaper ++ * @param \DOMDocumentFactory|null $domDocumentFactory + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Sales\Model\Config $salesConfig, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, +- \Magento\Framework\Escaper $escaper ++ \Magento\Framework\Escaper $escaper, ++ \DOMDocumentFactory $domDocumentFactory = null + ) { + $this->priceCurrency = $priceCurrency; + $this->_storeManager = $storeManager; + $this->_salesConfig = $salesConfig; + $this->escaper = $escaper; ++ $this->domDocumentFactory = $domDocumentFactory ++ ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); + parent::__construct($context); + } + +@@ -149,30 +160,41 @@ + public function escapeHtmlWithLinks($data, $allowedTags = null) + { + if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { +- $links = []; +- $i = 1; +- $data = str_replace('%', '%%', $data); +- $regexp = "#(?J).*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" +- .">?(?:(?:(?.*?)(?:<\/a\s*>?|(?=<\w))|(?.*)))#si"; +- while (preg_match($regexp, $data, $matches)) { +- $text = ''; +- if (!empty($matches['text'])) { +- $text = str_replace('%%', '%', $matches['text']); +- } +- $url = $this->filterUrl($matches['link'] ?? ''); +- //Recreate a minimalistic secure a tag +- $links[] = sprintf( +- '%s', +- htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), +- $this->escaper->escapeHtml($text) +- ); +- $data = str_replace($matches[0], '%' . $i . '$s', $data); +- ++$i; ++ $wrapperElementId = uniqid(); ++ $domDocument = $this->domDocumentFactory->create(); ++ ++ $internalErrors = libxml_use_internal_errors(true); ++ ++ $domDocument->loadHTML( ++ '' . $data . '' ++ ); ++ ++ libxml_use_internal_errors($internalErrors); ++ ++ $linkTags = $domDocument->getElementsByTagName('a'); ++ ++ foreach ($linkTags as $linkNode) { ++ $linkAttributes = []; ++ foreach ($linkNode->attributes as $attribute) { ++ $linkAttributes[$attribute->name] = $attribute->value; ++ } ++ ++ foreach ($linkAttributes as $attributeName => $attributeValue) { ++ if ($attributeName === 'href') { ++ $url = $this->filterUrl($attributeValue ?? ''); ++ $url = $this->escaper->escapeUrl($url); ++ $linkNode->setAttribute('href', $url); ++ } else { ++ $linkNode->removeAttribute($attributeName); ++ } ++ } + } +- $data = $this->escaper->escapeHtml($data, $allowedTags); +- return vsprintf($data, $links); ++ ++ $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); ++ preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); ++ $data = !empty($matches) ? $matches[1] : ''; + } ++ + return $this->escaper->escapeHtml($data, $allowedTags); + } + +@@ -186,7 +208,6 @@ + { + if ($url) { + //Revert the sprintf escaping +- $url = str_replace('%%', '%', $url); + $urlScheme = parse_url($url, PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { +Index: vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision d46f8efde7d6e7c40859e833f462c30edc101476) ++++ vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision ab7bbf5f3f2ef7827e7f9eb0ea88a06144f36a94) +@@ -71,7 +71,7 @@ + ->disableOriginalConstructor() + ->getMock(); + +- $this->adminHelper = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( ++ $this->adminHelper = (new ObjectManager($this))->getObject( + \Magento\Sales\Helper\Admin::class, + [ + 'context' => $this->contextMock, +@@ -330,72 +330,16 @@ + } + + /** +- * @param string $data +- * @param string $expected +- * @param null|array $allowedTags +- * @dataProvider escapeHtmlWithLinksDataProvider ++ * @return void + */ +- public function testEscapeHtmlWithLinks($data, $expected, $allowedTags = null) ++ public function testEscapeHtmlWithLinks() + { ++ $expected = '<a>some text in tags</a>'; + $this->escaperMock + ->expects($this->any()) + ->method('escapeHtml') + ->will($this->returnValue($expected)); +- $actual = $this->adminHelper->escapeHtmlWithLinks($data, $allowedTags); ++ $actual = $this->adminHelper->escapeHtmlWithLinks('some text in tags'); + $this->assertEquals($expected, $actual); + } +- +- /** +- * @return array +- */ +- public function escapeHtmlWithLinksDataProvider() +- { +- return [ +- [ +- 'some text in tags', +- '<a>some text in tags</a>', +- 'allowedTags' => null +- ], +- [ +- 'Transaction ID: "XX123XX"', +- 'Transaction ID: "XX123XX"', +- 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'] +- ], +- [ +- 'some text in tags', +- 'some text in tags', +- 'allowedTags' => ['a'] +- ], +- 'Not replacement with placeholders' => [ +- "", +- '<script>alert(1)</script>', +- 'allowedTags' => ['a'] +- ], +- 'Normal usage, url escaped' => [ +- 'Foo', +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Normal usage, url not escaped' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'XSS test' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Additional regex test' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Break of valid urls' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- ]; +- } + } diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch new file mode 100644 index 0000000..4705e12 --- /dev/null +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch @@ -0,0 +1,217 @@ +Index: vendor/magento/module-sales/Helper/Admin.php +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-sales/Helper/Admin.php (revision 3b69a3b12138bbad688dde2bd89af36aef0cf967) ++++ vendor/magento/module-sales/Helper/Admin.php (revision 1f7017a41bddae5d538eb78e56cbd6374d158503) +@@ -7,6 +7,8 @@ + + namespace Magento\Sales\Helper; + ++use Magento\Framework\App\ObjectManager; ++ + /** + * Sales admin helper. + */ +@@ -32,24 +34,33 @@ + */ + protected $escaper; + ++ /** ++ * @var \DOMDocumentFactory ++ */ ++ private $domDocumentFactory; ++ + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Sales\Model\Config $salesConfig + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Framework\Escaper $escaper ++ * @param \DOMDocumentFactory|null $domDocumentFactory + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Sales\Model\Config $salesConfig, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, +- \Magento\Framework\Escaper $escaper ++ \Magento\Framework\Escaper $escaper, ++ \DOMDocumentFactory $domDocumentFactory = null + ) { + $this->priceCurrency = $priceCurrency; + $this->_storeManager = $storeManager; + $this->_salesConfig = $salesConfig; + $this->escaper = $escaper; ++ $this->domDocumentFactory = $domDocumentFactory ++ ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); + parent::__construct($context); + } + +@@ -150,30 +161,41 @@ + public function escapeHtmlWithLinks($data, $allowedTags = null) + { + if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { +- $links = []; +- $i = 1; +- $data = str_replace('%', '%%', $data); +- $regexp = "#(?J).*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" +- .">?(?:(?:(?.*?)(?:<\/a\s*>?|(?=<\w))|(?.*)))#si"; +- while (preg_match($regexp, $data, $matches)) { +- $text = ''; +- if (!empty($matches['text'])) { +- $text = str_replace('%%', '%', $matches['text']); +- } +- $url = $this->filterUrl($matches['link'] ?? ''); +- //Recreate a minimalistic secure a tag +- $links[] = sprintf( +- '%s', +- htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), +- $this->escaper->escapeHtml($text) +- ); +- $data = str_replace($matches[0], '%' . $i . '$s', $data); +- ++$i; ++ $wrapperElementId = uniqid(); ++ $domDocument = $this->domDocumentFactory->create(); ++ ++ $internalErrors = libxml_use_internal_errors(true); ++ ++ $domDocument->loadHTML( ++ '' . $data . '' ++ ); ++ ++ libxml_use_internal_errors($internalErrors); ++ ++ $linkTags = $domDocument->getElementsByTagName('a'); ++ ++ foreach ($linkTags as $linkNode) { ++ $linkAttributes = []; ++ foreach ($linkNode->attributes as $attribute) { ++ $linkAttributes[$attribute->name] = $attribute->value; ++ } ++ ++ foreach ($linkAttributes as $attributeName => $attributeValue) { ++ if ($attributeName === 'href') { ++ $url = $this->filterUrl($attributeValue ?? ''); ++ $url = $this->escaper->escapeUrl($url); ++ $linkNode->setAttribute('href', $url); ++ } else { ++ $linkNode->removeAttribute($attributeName); ++ } ++ } + } +- $data = $this->escaper->escapeHtml($data, $allowedTags); +- return vsprintf($data, $links); ++ ++ $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); ++ preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); ++ $data = !empty($matches) ? $matches[1] : ''; + } ++ + return $this->escaper->escapeHtml($data, $allowedTags); + } + +@@ -187,7 +209,7 @@ + { + if ($url) { + //Revert the sprintf escaping +- $url = str_replace('%%', '%', $url); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlScheme = parse_url($url, PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { +Index: vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision 3b69a3b12138bbad688dde2bd89af36aef0cf967) ++++ vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision f18c17278c29c594bc729b0da2e6e91150ab6541) +@@ -71,7 +71,7 @@ + ->disableOriginalConstructor() + ->getMock(); + +- $this->adminHelper = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( ++ $this->adminHelper = (new ObjectManager($this))->getObject( + \Magento\Sales\Helper\Admin::class, + [ + 'context' => $this->contextMock, +@@ -330,72 +330,16 @@ + } + + /** +- * @param string $data +- * @param string $expected +- * @param null|array $allowedTags +- * @dataProvider escapeHtmlWithLinksDataProvider ++ * @return void + */ +- public function testEscapeHtmlWithLinks($data, $expected, $allowedTags = null) ++ public function testEscapeHtmlWithLinks(): void + { ++ $expected = '<a>some text in tags</a>'; + $this->escaperMock + ->expects($this->any()) + ->method('escapeHtml') + ->will($this->returnValue($expected)); +- $actual = $this->adminHelper->escapeHtmlWithLinks($data, $allowedTags); ++ $actual = $this->adminHelper->escapeHtmlWithLinks('some text in tags'); + $this->assertEquals($expected, $actual); + } +- +- /** +- * @return array +- */ +- public function escapeHtmlWithLinksDataProvider() +- { +- return [ +- [ +- 'some text in tags', +- '<a>some text in tags</a>', +- 'allowedTags' => null +- ], +- [ +- 'Transaction ID: "XX123XX"', +- 'Transaction ID: "XX123XX"', +- 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'] +- ], +- [ +- 'some text in tags', +- 'some text in tags', +- 'allowedTags' => ['a'] +- ], +- 'Not replacement with placeholders' => [ +- "", +- '<script>alert(1)</script>', +- 'allowedTags' => ['a'] +- ], +- 'Normal usage, url escaped' => [ +- 'Foo', +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Normal usage, url not escaped' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'XSS test' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Additional regex test' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Break of valid urls' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- ]; +- } + } diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch new file mode 100644 index 0000000..501b478 --- /dev/null +++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch @@ -0,0 +1,35 @@ +--- + vendor/magento/framework/App/Router/ActionList.php | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +index ea22fd6ab5b..ada0489ba90 100644 +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -1,6 +1,5 @@ + Date: Sat, 23 Nov 2019 18:57:19 -0600 Subject: [PATCH 2/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- ...233__fix_xss_in_order_history__2.2.0.patch | 93 ------------------- 1 file changed, 93 deletions(-) diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch index 6b7cf93..1a4457e 100644 --- a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch @@ -121,96 +121,3 @@ Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP $urlScheme = parse_url($url, PHP_URL_SCHEME); $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; if ($urlScheme !== 'http' && $urlScheme !== 'https') { -Index: vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision d46f8efde7d6e7c40859e833f462c30edc101476) -+++ vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision ab7bbf5f3f2ef7827e7f9eb0ea88a06144f36a94) -@@ -71,7 +71,7 @@ - ->disableOriginalConstructor() - ->getMock(); - -- $this->adminHelper = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( -+ $this->adminHelper = (new ObjectManager($this))->getObject( - \Magento\Sales\Helper\Admin::class, - [ - 'context' => $this->contextMock, -@@ -330,72 +330,16 @@ - } - - /** -- * @param string $data -- * @param string $expected -- * @param null|array $allowedTags -- * @dataProvider escapeHtmlWithLinksDataProvider -+ * @return void - */ -- public function testEscapeHtmlWithLinks($data, $expected, $allowedTags = null) -+ public function testEscapeHtmlWithLinks() - { -+ $expected = '<a>some text in tags</a>'; - $this->escaperMock - ->expects($this->any()) - ->method('escapeHtml') - ->will($this->returnValue($expected)); -- $actual = $this->adminHelper->escapeHtmlWithLinks($data, $allowedTags); -+ $actual = $this->adminHelper->escapeHtmlWithLinks('some text in tags'); - $this->assertEquals($expected, $actual); - } -- -- /** -- * @return array -- */ -- public function escapeHtmlWithLinksDataProvider() -- { -- return [ -- [ -- 'some text in tags', -- '<a>some text in tags</a>', -- 'allowedTags' => null -- ], -- [ -- 'Transaction ID: "XX123XX"', -- 'Transaction ID: "XX123XX"', -- 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'] -- ], -- [ -- 'some text in tags', -- 'some text in tags', -- 'allowedTags' => ['a'] -- ], -- 'Not replacement with placeholders' => [ -- "", -- '<script>alert(1)</script>', -- 'allowedTags' => ['a'] -- ], -- 'Normal usage, url escaped' => [ -- 'Foo', -- 'Foo', -- 'allowedTags' => ['a'] -- ], -- 'Normal usage, url not escaped' => [ -- "Foo", -- 'Foo', -- 'allowedTags' => ['a'] -- ], -- 'XSS test' => [ -- "Foo", -- 'Foo', -- 'allowedTags' => ['a'] -- ], -- 'Additional regex test' => [ -- "Foo", -- 'Foo', -- 'allowedTags' => ['a'] -- ], -- 'Break of valid urls' => [ -- "Foo", -- 'Foo', -- 'allowedTags' => ['a'] -- ], -- ]; -- } - } From 358baae0e7419b8557c2a5cc55d1a0cc0afa100a Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Mon, 2 Dec 2019 08:54:13 -0600 Subject: [PATCH 3/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- patches.json | 6 +++--- ...atch => PRODSECBUG-2198__preauth_sql__2.1.17.patch} | 10 +++------- ...patch => PRODSECBUG-2198__preauth_sql__2.2.8.patch} | 6 ++---- ...patch => PRODSECBUG-2198__preauth_sql__2.3.1.patch} | 6 ++---- ...DSECBUG-2233__fix_xss_in_order_history__2.2.0.patch | 10 +++------- ...SECBUG-2432__admin_path_disclosure_bug__2.1.4.patch | 7 +------ 6 files changed, 14 insertions(+), 31 deletions(-) rename patches/{PRODSECBUG-2198__preauth_sql__2.1.4.patch => PRODSECBUG-2198__preauth_sql__2.1.17.patch} (59%) rename patches/{PRODSECBUG-2198__preauth_sql__2.2.0.patch => PRODSECBUG-2198__preauth_sql__2.2.8.patch} (95%) rename patches/{PRODSECBUG-2198__preauth_sql__2.3.0.patch => PRODSECBUG-2198__preauth_sql__2.3.1.patch} (98%) diff --git a/patches.json b/patches.json index 01f6e38..699e0ea 100644 --- a/patches.json +++ b/patches.json @@ -220,9 +220,9 @@ "2.3.0 - 2.3.2": "PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch" }, "Pre-auth SQL Injection": { - "2.1.4 - 2.1.17": "PRODSECBUG-2198__preauth_sql__2.1.4.patch", - "2.2.0 - 2.2.7": "PRODSECBUG-2198__preauth_sql__2.2.0.patch", - "2.3.0": "PRODSECBUG-2198__preauth_sql__2.3.0.patch" + "2.1.17": "PRODSECBUG-2198__preauth_sql__2.1.17.patch", + "2.2.8": "PRODSECBUG-2198__preauth_sql__2.2.8.patch", + "2.3.1": "PRODSECBUG-2198__preauth_sql__2.3.1.patch" }, "Transaction MD5 hash field is removed by Authorize.net": { "2.2.0 - 2.2.7": "MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch" diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch b/patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch similarity index 59% rename from patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch rename to patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch index 0f77f18..de97726 100644 --- a/patches/PRODSECBUG-2198__preauth_sql__2.1.4.patch +++ b/patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch @@ -1,10 +1,6 @@ -Index: vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/framework/DB/Adapter/Pdo/Mysql.php (revision 6a8701ca9402697f5eaf022e35b9217d3281546c) -+++ vendor/magento/framework/DB/Adapter/Pdo/Mysql.php (date 1553502112000) +diff -Nuar a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php @@ -2796,7 +2796,7 @@ if (isset($condition['to'])) { $query .= empty($query) ? '' : ' AND '; diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch b/patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch similarity index 95% rename from patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch rename to patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch index 6ad7304..65645c2 100644 --- a/patches/PRODSECBUG-2198__preauth_sql__2.2.0.patch +++ b/patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php -index 7a1926c..331c667 100644 +diff -Nuar a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php --- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -138,7 +138,9 @@ class Synchronizer @@ -78,8 +77,7 @@ index 7a1926c..331c667 100644 } } -diff --git a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -index 1449d6d..38085a3 100644 +diff -Nuar a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php --- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php @@ -2904,7 +2904,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch b/patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch similarity index 98% rename from patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch rename to patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch index e778277..e5bafb9 100644 --- a/patches/PRODSECBUG-2198__preauth_sql__2.3.0.patch +++ b/patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php -index 3ec8e96..72f6628 100644 +diff -Nuar a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php --- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -16,6 +16,8 @@ use Magento\Customer\Model\Visitor; @@ -110,8 +109,7 @@ index 3ec8e96..72f6628 100644 } } -diff --git a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -index 441da10..69505d4 100644 +diff -Nuar a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php --- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php @@ -515,6 +515,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch index 1a4457e..a367b77 100644 --- a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch @@ -1,10 +1,6 @@ -Index: vendor/magento/module-sales/Helper/Admin.php -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-sales/Helper/Admin.php (revision d46f8efde7d6e7c40859e833f462c30edc101476) -+++ vendor/magento/module-sales/Helper/Admin.php (revision 102b397919b1c82aa13118abb5345ad81c626370) +diff -Nuar a/vendor/magento/module-sales/Helper/Admin.php b/vendor/magento/module-sales/Helper/Admin.php +--- a/vendor/magento/module-sales/Helper/Admin.php ++++ b/vendor/magento/module-sales/Helper/Admin.php @@ -6,6 +6,8 @@ namespace Magento\Sales\Helper; diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch index 501b478..f7e4612 100644 --- a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch +++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch @@ -1,9 +1,4 @@ ---- - vendor/magento/framework/App/Router/ActionList.php | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php -index ea22fd6ab5b..ada0489ba90 100644 +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php --- a/vendor/magento/framework/App/Router/ActionList.php +++ b/vendor/magento/framework/App/Router/ActionList.php @@ -1,6 +1,5 @@ From 5fb887b3d6b9129ff6b142af209ccf5b152fc766 Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Mon, 2 Dec 2019 11:10:56 -0600 Subject: [PATCH 4/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- patches.json | 9 +- ...ulti-site_configuration_issue__2.2.4.patch | 3 +- ...nnot_change_the_applied_theme__2.2.5.patch | 3 +- ...retrieving_product_attributes__2.2.5.patch | 12 +- ...91__customer_attributes_issue__2.2.6.patch | 46 +-- ...d_is_removed_by_authorize_net__2.2.0.patch | 45 +-- ...e_view_scope_in async_web_api__2.3.1.patch | 20 +- ...e_view_scope_in async_web_api__2.3.2.patch | 20 +- ...PRODSECBUG-2198__preauth_sql__2.1.17.patch | 12 - .../PRODSECBUG-2198__preauth_sql__2.2.8.patch | 94 ------ .../PRODSECBUG-2198__preauth_sql__2.3.1.patch | 310 ------------------ ...33__fix_xss_in_order_history__2.2.7.patch} | 0 ...233__fix_xss_in_order_history__2.3.0.patch | 20 +- ...32__admin_path_disclosure_bug__2.1.4.patch | 9 - ...32__admin_path_disclosure_bug__2.2.0.patch | 16 +- ...32__admin_path_disclosure_bug__2.3.0.patch | 16 +- 16 files changed, 63 insertions(+), 572 deletions(-) delete mode 100644 patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch delete mode 100644 patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch delete mode 100644 patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch rename patches/{PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch => PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch} (100%) diff --git a/patches.json b/patches.json index 699e0ea..d0f7ccb 100644 --- a/patches.json +++ b/patches.json @@ -207,7 +207,7 @@ "2.3.2": "MDVA-22979__fix_pagebuilder_module__2.3.2.patch" }, "Fix XSS in order history": { - "2.2.0 - 2.2.8": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch", + "2.2.7 - 2.2.8": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch", "2.3.0 - 2.3.1": "PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch" }, "Pass Store View scope in the Async/Bulk Web API": { @@ -219,11 +219,6 @@ ">=2.2.0 <2.3.0": "PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch", "2.3.0 - 2.3.2": "PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch" }, - "Pre-auth SQL Injection": { - "2.1.17": "PRODSECBUG-2198__preauth_sql__2.1.17.patch", - "2.2.8": "PRODSECBUG-2198__preauth_sql__2.2.8.patch", - "2.3.1": "PRODSECBUG-2198__preauth_sql__2.3.1.patch" - }, "Transaction MD5 hash field is removed by Authorize.net": { "2.2.0 - 2.2.7": "MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch" }, @@ -234,7 +229,7 @@ "2.2.5": "MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch" }, "Cannot change the applied theme": { - "2.2.4 - 2.2.5": "MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch" + "2.2.5": "MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch" }, "Fix for multi-site configuration issue": { "2.2.4": "MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch" diff --git a/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch b/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch index 8fdf994..4a47ef5 100644 --- a/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch +++ b/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch @@ -1,5 +1,4 @@ -diff --git a/app/etc/di.xml b/app/etc/di.xml -index de50840..4eaa739 100644 +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -232,6 +233,11 @@ diff --git a/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch b/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch index 659da92..cb5fdb4 100644 --- a/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch +++ b/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-email/Model/AbstractTemplate.php b/vendor/magento/module-email/Model/AbstractTemplate.php -index fa9d28074bf8..81e993fad76d 100644 +diff -Nuar a/vendor/magento/module-email/Model/AbstractTemplate.php b/vendor/magento/module-email/Model/AbstractTemplate.php --- a/vendor/magento/module-email/Model/AbstractTemplate.php +++ b/vendor/magento/module-email/Model/AbstractTemplate.php @@ -534,10 +534,9 @@ protected function cancelDesignConfig() diff --git a/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch b/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch index b7a4f65..d35cf4e 100644 --- a/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch +++ b/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php -index 237ac96e56ec..24c3879d947b 100644 +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php --- a/vendor/magento/module-catalog/Model/Product.php +++ b/vendor/magento/module-catalog/Model/Product.php @@ -12,6 +12,7 @@ @@ -78,8 +77,7 @@ index 237ac96e56ec..24c3879d947b 100644 $this->customAttributesCodes = array_diff($this->customAttributesCodes, $this->interfaceAttributes); } return $this->customAttributesCodes; -diff --git a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php -index 4dae4ec68efa..ff4d2f93c912 100644 +diff -Nuar a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php --- a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +++ b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php @@ -58,7 +58,9 @@ public function afterExecute(ReadSnapshot $subject, array $entityData, $entityTy @@ -93,8 +91,7 @@ index 4dae4ec68efa..ff4d2f93c912 100644 /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ foreach ($attributes as $attribute) { -diff --git a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php -index 9febea9b7b2b..cd2fe7477ca6 100644 +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php --- a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php +++ b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php @@ -5,6 +5,7 @@ @@ -146,8 +143,7 @@ index 9febea9b7b2b..cd2fe7477ca6 100644 if (!$attribute->isStatic()) { $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); $attributesMap[$attribute->getAttributeId()] = $attribute->getAttributeCode(); -diff --git a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php -index 0569a249260f..4b9dbab7faaa 100644 +diff -Nuar a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php --- a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php +++ b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php @@ -69,7 +69,7 @@ public function beforeGetImage( diff --git a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch index 2554dc9..8d9ff62 100644 --- a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch +++ b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php -index b9f9b739f4fa..fcd0fbc03278 100644 +diff -Nuar a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php --- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -18,6 +18,8 @@ @@ -138,8 +137,7 @@ index b9f9b739f4fa..fcd0fbc03278 100644 * @param string $path * @param array $params * @param array $response -diff --git a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php -index 7fe012a87d92..e56428a1ae77 100644 +diff -Nuar a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php --- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -6,8 +6,15 @@ @@ -249,8 +247,7 @@ index 7fe012a87d92..e56428a1ae77 100644 * @param DataObject $response * @param array|null $options * @return $this -diff --git a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php -index f493cbc88f18..a1aaab0995d7 100644 +diff -Nuar a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php --- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -3,8 +3,10 @@ @@ -466,8 +463,7 @@ index f493cbc88f18..a1aaab0995d7 100644 + ->willReturnSelf(); + } } -diff --git a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php -index 9c747393cc72..750d38f60e13 100644 +diff -Nuar a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php --- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -9,6 +9,7 @@ @@ -843,8 +839,7 @@ index 9c747393cc72..750d38f60e13 100644 + $this->getModel()->execute(); + } } -diff --git a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php -index b85b03852b62..3a0b2b4bf722 100644 +diff -Nuar a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php --- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php @@ -9,8 +9,9 @@ @@ -903,8 +898,7 @@ index b85b03852b62..3a0b2b4bf722 100644 } /** -diff --git a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml -index 8a5f1919f78b..eeacc90fba91 100644 +diff -Nuar a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml --- a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +++ b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml @@ -40,13 +40,16 @@ function getFrontTab() { @@ -925,8 +919,7 @@ index 8a5f1919f78b..eeacc90fba91 100644 } } } -diff --git a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js -index 6ea005915763..7adc0dcfdf40 100644 +diff -Nuar a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js --- a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js +++ b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js @@ -20,7 +20,6 @@ define([ @@ -1004,8 +997,7 @@ index 6ea005915763..7adc0dcfdf40 100644 }); window.attributeOption = attributeOption; window.optionDefaultInputType = attributeOption.getOptionInputType(); -diff --git a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php -index 383c97a166d3..72d27152d639 100644 +diff -Nuar a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php --- a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +++ b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php @@ -16,6 +16,8 @@ @@ -1033,8 +1025,7 @@ index 383c97a166d3..72d27152d639 100644 switch ($data['frontend_input']) { case 'swatch_visual': $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL; -diff --git a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js -index 1a58e4b6f2e7..6ef046ae1955 100644 +diff -Nuar a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js --- a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js +++ b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js @@ -16,7 +16,8 @@ define([ @@ -1133,8 +1124,7 @@ index 1a58e4b6f2e7..6ef046ae1955 100644 }); }); -diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php -index f1086f4871f3..2d9dea375e97 100644 +diff -Nuar a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php @@ -91,15 +91,18 @@ public function persist(FixtureInterface $fixture = null) @@ -1201,8 +1191,7 @@ index f1086f4871f3..2d9dea375e97 100644 + return json_encode($options); + } } -diff --git a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php -index 083fa246c96e..f4adb9dec125 100644 +diff -Nuar a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php --- a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php @@ -29,21 +29,32 @@ public function __construct(DataInterface $configuration, EventManagerInterface @@ -1248,8 +1237,7 @@ index 083fa246c96e..f4adb9dec125 100644 + return json_encode($options); } } -diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php -index 098b18d6f38c..45c1583d7640 100644 +diff -Nuar a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -282,13 +282,15 @@ public function testLargeOptionsDataSet() @@ -1274,8 +1262,7 @@ index 098b18d6f38c..45c1583d7640 100644 } $attributeData['serialized_options'] = json_encode($optionsData); $this->getRequest()->setPostValue($attributeData); -diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php -index be9fd96d7589..493c4dcadc1e 100644 +diff -Nuar a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php --- a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php @@ -7,6 +7,8 @@ @@ -1405,7 +1392,7 @@ index be9fd96d7589..493c4dcadc1e 100644 $this->dispatch('backend/catalog/product_attribute/save'); $entityTypeId = $this->_objectManager->create( \Magento\Eav\Model\Entity::class -diff --git a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor/magento/framework/Serialize/Serializer/FormData.php +diff -Nuar a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor/magento/framework/Serialize/Serializer/FormData.php new file mode 100644 index 000000000000..077e91797d2b --- /dev/null @@ -1466,7 +1453,7 @@ index 000000000000..077e91797d2b + return $formData; + } +} -diff --git a/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php +diff -Nuar a/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php new file mode 100644 index 000000000000..7729a2da97ef --- /dev/null @@ -1578,8 +1565,7 @@ index 000000000000..7729a2da97ef + $this->formDataSerializer->unserialize($serializedData); + } +} -diff --git a/lib/web/mage/backend/validation.js b/lib/web/mage/backend/validation.js -index d3ab7dd086a4..f141fb3eeb8d 100644 +diff -Nuar a/lib/web/mage/backend/validation.js b/lib/web/mage/backend/validation.js --- a/lib/web/mage/backend/validation.js +++ b/lib/web/mage/backend/validation.js @@ -171,6 +171,7 @@ diff --git a/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch index 06e9137..be518ee 100644 --- a/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch +++ b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch @@ -1,7 +1,6 @@ -Index: vendor/magento/module-authorizenet/Model/Directpost.php -<+>UTF-8 -=================================================================== ---- vendor/magento/module-authorizenet/Model/Directpost.php (revision 01fbfeba9bd743266199e30260376a9c1b95frevision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +diff -Nuar a/vendor/magento/module-authorizenet/Model/Directpost.php b/vendor/magento/module-authorizenet/Model/Directpost.php +--- a/vendor/magento/module-authorizenet/Model/Directpost.php ++++ b/vendor/magento/module-authorizenet/Model/Directpost.php @@ -543,15 +543,16 @@ public function validateResponse() { @@ -23,11 +22,9 @@ Index: vendor/magento/module-authorizenet/Model/Directpost.php return true; } -Index: vendor/magento/module-authorizenet/Model/Directpost/Request.php -<+>UTF-8 -=================================================================== ---- vendor/magento/module-authorizenet/Model/Directpost/Request.php (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) -+++ vendor/magento/module-authorizenet/Model/Directpost/Request.php (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +diff -Nuar a/vendor/magento/module-authorizenet/Model/Directpost/Request.php b/vendor/magento/module-authorizenet/Model/Directpost/Request.php +--- a/vendor/magento/module-authorizenet/Model/Directpost/Request.php ++++ b/vendor/magento/module-authorizenet/Model/Directpost/Request.php @@ -7,6 +7,7 @@ namespace Magento\Authorizenet\Model\Directpost; @@ -197,13 +194,9 @@ Index: vendor/magento/module-authorizenet/Model/Directpost/Request.php + $this->signatureKey = $signatureKey; + } } -Index: vendor/magento/module-authorizenet/Model/Directpost/Response.php -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-authorizenet/Model/Directpost/Response.php (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) -+++ vendor/magento/module-authorizenet/Model/Directpost/Response.php (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +diff -Nuar a/vendor/magento/module-authorizenet/Model/Directpost/Response.php b/vendor/magento/module-authorizenet/Model/Directpost/Response.php +--- a/vendor/magento/module-authorizenet/Model/Directpost/Response.php ++++ b/vendor/magento/module-authorizenet/Model/Directpost/Response.php @@ -24,27 +24,33 @@ */ public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) @@ -302,13 +295,9 @@ Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP + return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); + } } -Index: vendor/magento/module-authorizenet/etc/adminhtml/system.xml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-authorizenet/etc/adminhtml/system.xml (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) -+++ vendor/magento/module-authorizenet/etc/adminhtml/system.xml (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +diff -Nuar a/vendor/magento/module-authorizenet/etc/adminhtml/system.xml b/vendor/magento/module-authorizenet/etc/adminhtml/system.xml +--- a/vendor/magento/module-authorizenet/etc/adminhtml/system.xml ++++ b/vendor/magento/module-authorizenet/etc/adminhtml/system.xml @@ -29,6 +29,10 @@ Magento\Config\Model\Config\Backend\Encrypted @@ -320,13 +309,9 @@ Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP Magento\Config\Model\Config\Backend\Encrypted -Index: vendor/magento/module-authorizenet/etc/config.xml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-authorizenet/etc/config.xml (revision 01fbfeba9bd743266199e30260376a9c1b95fbf5) -+++ vendor/magento/module-authorizenet/etc/config.xml (revision a0dade4d8bc15b651d8b1ea0e7e4e3580fb1d3ae) +diff -Nuar a/vendor/magento/module-authorizenet/etc/config.xml b/vendor/magento/module-authorizenet/etc/config.xml +--- a/vendor/magento/module-authorizenet/etc/config.xml ++++ b/vendor/magento/module-authorizenet/etc/config.xml @@ -22,6 +22,7 @@ Credit Card Direct Post (Authorize.net) diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch index 47644db..324ac3f 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch @@ -1,4 +1,4 @@ -diff --git a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php new file mode 100644 index 00000000000..c9899663487 --- /dev/null @@ -106,7 +106,7 @@ index 00000000000..c9899663487 + } + } +} -diff --git a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +diff -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php new file mode 100644 index 00000000000..9c54c8485db --- /dev/null @@ -227,8 +227,7 @@ index 00000000000..9c54c8485db + return [$topic, $envelopes]; + } +} -diff --git a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json -index b50e951b46f..54ebfde3b68 100644 +diff -Nuar a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json --- a/vendor/magento/module-amqp/composer.json +++ b/vendor/magento/module-amqp/composer.json @@ -8,6 +11,10 @@ @@ -244,8 +243,7 @@ index b50e951b46f..54ebfde3b68 100644 "type": "magento2-module", "license": [ "OSL-3.0", -diff --git a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml -index 920bb72261e..8bcf290f4a9 100644 +diff -Nuar a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml --- a/vendor/magento/module-amqp/etc/di.xml +++ b/vendor/magento/module-amqp/etc/di.xml @@ -72,4 +72,10 @@ @@ -259,8 +257,7 @@ index 920bb72261e..8bcf290f4a9 100644 + + -diff --git a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml -index 1768a9b121c..5f49dae67f9 100644 +diff -Nuar a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml --- a/vendor/magento/module-amqp/etc/module.xml +++ b/vendor/magento/module-amqp/etc/module.xml @@ -6,5 +6,9 @@ @@ -274,8 +271,7 @@ index 1768a9b121c..5f49dae67f9 100644 + + -diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php -index af1ef4400e4..618b16f549e 100644 +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php --- a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php @@ -8,17 +8,10 @@ declare(strict_types=1); @@ -406,7 +402,7 @@ index af1ef4400e4..618b16f549e 100644 }; } } -diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php new file mode 100644 index 00000000000..1327fcc2672 --- /dev/null @@ -550,7 +546,7 @@ index 00000000000..1327fcc2672 + return $this->queue; + } +} -diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +diff -Nuar a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php new file mode 100644 index 00000000000..61ec73990c9 --- /dev/null diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch index 911fcf6..e015e39 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch @@ -1,4 +1,4 @@ -diff --git a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php new file mode 100644 index 00000000000..c9899663487 --- /dev/null @@ -106,7 +106,7 @@ index 00000000000..c9899663487 + } + } +} -diff --git a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +diff -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php new file mode 100644 index 00000000000..9c54c8485db --- /dev/null @@ -227,8 +227,7 @@ index 00000000000..9c54c8485db + return [$topic, $envelopes]; + } +} -diff --git a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json -index b50e951b46f..54ebfde3b68 100644 +diff -Nuar a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json --- a/vendor/magento/module-amqp/composer.json +++ b/vendor/magento/module-amqp/composer.json @@ -8,6 +11,10 @@ @@ -244,8 +243,7 @@ index b50e951b46f..54ebfde3b68 100644 "type": "magento2-module", "license": [ "OSL-3.0", -diff --git a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml -index 920bb72261e..8bcf290f4a9 100644 +diff -Nuar a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml --- a/vendor/magento/module-amqp/etc/di.xml +++ b/vendor/magento/module-amqp/etc/di.xml @@ -72,4 +72,10 @@ @@ -259,8 +257,7 @@ index 920bb72261e..8bcf290f4a9 100644 + + -diff --git a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml -index 1768a9b121c..5f49dae67f9 100644 +diff -Nuar a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml --- a/vendor/magento/module-amqp/etc/module.xml +++ b/vendor/magento/module-amqp/etc/module.xml @@ -6,5 +6,9 @@ @@ -274,8 +271,7 @@ index 1768a9b121c..5f49dae67f9 100644 + + -diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php -index af1ef4400e4..618b16f549e 100644 +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php --- a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php @@ -8,18 +8,11 @@ declare(strict_types=1); @@ -420,7 +416,7 @@ index af1ef4400e4..618b16f549e 100644 }; } } -diff --git a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php new file mode 100644 index 00000000000..1327fcc2672 --- /dev/null @@ -564,7 +560,7 @@ index 00000000000..1327fcc2672 + return $this->queue; + } +} -diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +diff -Nuar a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php new file mode 100644 index 00000000000..61ec73990c9 --- /dev/null diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch b/patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch deleted file mode 100644 index de97726..0000000 --- a/patches/PRODSECBUG-2198__preauth_sql__2.1.17.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff -Nuar a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ---- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -+++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -@@ -2796,7 +2796,7 @@ - if (isset($condition['to'])) { - $query .= empty($query) ? '' : ' AND '; - $to = $this->_prepareSqlDateCondition($condition, 'to'); -- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); -+ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); - } - } elseif (array_key_exists($key, $conditionKeyMap)) { - $value = $condition[$key]; diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch b/patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch deleted file mode 100644 index 65645c2..0000000 --- a/patches/PRODSECBUG-2198__preauth_sql__2.2.8.patch +++ /dev/null @@ -1,94 +0,0 @@ -diff -Nuar a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php ---- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php -+++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php -@@ -138,7 +138,9 @@ class Synchronizer - $productIds = []; - - foreach ($actions as $action) { -- $productIds[] = $action['product_id']; -+ if (isset($action['product_id']) && is_int($action['product_id'])) { -+ $productIds[] = $action['product_id']; -+ } - } - - return $productIds; -@@ -159,33 +161,37 @@ class Synchronizer - $customerId = $this->session->getCustomerId(); - $visitorId = $this->visitor->getId(); - $collection = $this->getActionsByType($typeId); -- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); -- -- /** -- * Note that collection is also filtered by visitor id and customer id -- * This collection shouldnt be flushed when visitor has products and then login -- * It can remove only products for visitor, or only products for customer -- * -- * ['product_id' => 'added_at'] -- * @var ProductFrontendActionInterface $item -- */ -- foreach ($collection as $item) { -- $this->entityManager->delete($item); -- } -- -- foreach ($productsData as $productId => $productData) { -- /** @var ProductFrontendActionInterface $action */ -- $action = $this->productFrontendActionFactory->create([ -- 'data' => [ -- 'visitor_id' => $customerId ? null : $visitorId, -- 'customer_id' => $this->session->getCustomerId(), -- 'added_at' => $productData['added_at'], -- 'product_id' => $productId, -- 'type_id' => $typeId -- ] -- ]); -- -- $this->entityManager->save($action); -+ $productIds = $this->getProductIdsByActions($productsData); -+ -+ if ($productIds) { -+ $collection->addFieldToFilter('product_id', $productIds); -+ -+ /** -+ * Note that collection is also filtered by visitor id and customer id -+ * This collection shouldn't be flushed when visitor has products and then login -+ * It can remove only products for visitor, or only products for customer -+ * -+ * ['product_id' => 'added_at'] -+ * @var ProductFrontendActionInterface $item -+ */ -+ foreach ($collection as $item) { -+ $this->entityManager->delete($item); -+ } -+ -+ foreach ($productsData as $productId => $productData) { -+ /** @var ProductFrontendActionInterface $action */ -+ $action = $this->productFrontendActionFactory->create([ -+ 'data' => [ -+ 'visitor_id' => $customerId ? null : $visitorId, -+ 'customer_id' => $this->session->getCustomerId(), -+ 'added_at' => $productData['added_at'], -+ 'product_id' => $productId, -+ 'type_id' => $typeId -+ ] -+ ]); -+ -+ $this->entityManager->save($action); -+ } - } - } - -diff -Nuar a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ---- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -+++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -@@ -2904,7 +2904,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - if (isset($condition['to'])) { - $query .= empty($query) ? '' : ' AND '; - $to = $this->_prepareSqlDateCondition($condition, 'to'); -- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); -+ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); - } - } elseif (array_key_exists($key, $conditionKeyMap)) { - $value = $condition[$key]; --- -2.7.4 - diff --git a/patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch b/patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch deleted file mode 100644 index e5bafb9..0000000 --- a/patches/PRODSECBUG-2198__preauth_sql__2.3.1.patch +++ /dev/null @@ -1,310 +0,0 @@ -diff -Nuar a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php ---- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php -+++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php -@@ -16,6 +16,8 @@ use Magento\Customer\Model\Visitor; - use Magento\Framework\EntityManager\EntityManager; - - /** -+ * A Product Widget Synchronizer. -+ * - * Service which allows to sync product widget information, such as product id with db. In order to reuse this info - * on different devices - */ -@@ -85,9 +87,10 @@ class Synchronizer - } - - /** -- * Find lifetime in configuration. Configuration is hold in Stores Configuration -- * Also this configuration is generated by: -- * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration -+ * Finds lifetime in configuration. -+ * -+ * Configuration is hold in Stores Configuration. Also this configuration is generated by -+ * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} - * - * @param string $namespace - * @return int -@@ -108,6 +111,8 @@ class Synchronizer - } - - /** -+ * Filters actions. -+ * - * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that - * can be persisted in database. - * -@@ -138,7 +143,9 @@ class Synchronizer - $productIds = []; - - foreach ($actions as $action) { -- $productIds[] = $action['product_id']; -+ if (isset($action['product_id']) && is_int($action['product_id'])) { -+ $productIds[] = $action['product_id']; -+ } - } - - return $productIds; -@@ -159,33 +166,37 @@ class Synchronizer - $customerId = $this->session->getCustomerId(); - $visitorId = $this->visitor->getId(); - $collection = $this->getActionsByType($typeId); -- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); -- -- /** -- * Note that collection is also filtered by visitor id and customer id -- * This collection shouldn't be flushed when visitor has products and then login -- * It can remove only products for visitor, or only products for customer -- * -- * ['product_id' => 'added_at'] -- * @var ProductFrontendActionInterface $item -- */ -- foreach ($collection as $item) { -- $this->entityManager->delete($item); -- } -- -- foreach ($productsData as $productId => $productData) { -- /** @var ProductFrontendActionInterface $action */ -- $action = $this->productFrontendActionFactory->create([ -- 'data' => [ -- 'visitor_id' => $customerId ? null : $visitorId, -- 'customer_id' => $this->session->getCustomerId(), -- 'added_at' => $productData['added_at'], -- 'product_id' => $productId, -- 'type_id' => $typeId -- ] -- ]); -- -- $this->entityManager->save($action); -+ $productIds = $this->getProductIdsByActions($productsData); -+ -+ if ($productIds) { -+ $collection->addFieldToFilter('product_id', $productIds); -+ -+ /** -+ * Note that collection is also filtered by visitor id and customer id -+ * This collection shouldn't be flushed when visitor has products and then login -+ * It can remove only products for visitor, or only products for customer -+ * -+ * ['product_id' => 'added_at'] -+ * @var ProductFrontendActionInterface $item -+ */ -+ foreach ($collection as $item) { -+ $this->entityManager->delete($item); -+ } -+ -+ foreach ($productsData as $productId => $productData) { -+ /** @var ProductFrontendActionInterface $action */ -+ $action = $this->productFrontendActionFactory->create([ -+ 'data' => [ -+ 'visitor_id' => $customerId ? null : $visitorId, -+ 'customer_id' => $this->session->getCustomerId(), -+ 'added_at' => $productData['added_at'], -+ 'product_id' => $productId, -+ 'type_id' => $typeId -+ ] -+ ]); -+ -+ $this->entityManager->save($action); -+ } - } - } - -diff -Nuar a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ---- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -+++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -@@ -515,6 +515,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Special handling for PDO query(). -+ * - * All bind parameter names must begin with ':'. - * - * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. -@@ -595,6 +596,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Special handling for PDO query(). -+ * - * All bind parameter names must begin with ':'. - * - * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. -@@ -615,8 +617,9 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - } - - /** -- * Allows multiple queries -- to safeguard against SQL injection, USE CAUTION and verify that input -- * cannot be tampered with. -+ * Allows multiple queries -- to safeguard against SQL injection, -+ * -+ * USE CAUTION and verify that input cannot be tampered with. - * - * Special handling for PDO query(). - * All bind parameter names must begin with ':'. -@@ -1278,6 +1281,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Modify tables, used for upgrade process -+ * - * Change columns definitions, reset foreign keys, change tables comments and engines. - * - * The value of each array element is an associative array -@@ -1469,11 +1473,11 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - * - * Method revrited for handle empty arrays in value param - * -- * @param string $text The text with a placeholder. -- * @param mixed $value The value to quote. -- * @param string $type OPTIONAL SQL datatype -+ * @param string $text The text with a placeholder. -+ * @param mixed $value The value to quote. -+ * @param string $type OPTIONAL SQL datatype - * @param integer $count OPTIONAL count of placeholders to replace -- * @return string An SQL-safe quoted value placed into the orignal text. -+ * @return string An SQL-safe quoted value placed into the original text. - */ - public function quoteInto($text, $value, $type = null, $count = null) - { -@@ -1514,6 +1518,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Load DDL data from cache -+ * - * Return false if cache does not exists - * - * @param string $tableCacheKey the table cache key -@@ -1568,7 +1573,8 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Reset cached DDL data from cache -- * if table name is null - reset all cached DDL data -+ * -+ * If table name is null - reset all cached DDL data - * - * @param string $tableName - * @param string $schemaName OPTIONAL -@@ -1605,6 +1611,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Disallow DDL caching -+ * - * @return $this - */ - public function disallowDdlCache() -@@ -1615,6 +1622,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Allow DDL caching -+ * - * @return $this - */ - public function allowDdlCache() -@@ -1675,6 +1683,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Format described column to definition, ready to be added to ddl table. -+ * - * Return array with keys: name, type, length, options, comment - * - * @param array $columnData -@@ -1892,6 +1901,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Inserts a table row with specified data -+ * - * Special for Zero values to identity column - * - * @param string $table -@@ -2782,6 +2792,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Add new Foreign Key to table -+ * - * If Foreign Key with same name is exist - it will be deleted - * - * @param string $fkName -@@ -2954,7 +2965,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - if (isset($condition['to'])) { - $query .= empty($query) ? '' : ' AND '; - $to = $this->_prepareSqlDateCondition($condition, 'to'); -- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); -+ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); - } - } elseif (array_key_exists($key, $conditionKeyMap)) { - $value = $condition[$key]; -@@ -3016,6 +3027,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Prepare value for save in column -+ * - * Return converted to column data type value - * - * @param array $column the column describe array -@@ -3140,7 +3152,8 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Generate fragment of SQL, that check value against multiple condition cases -- * and return different result depends on them -+ * -+ * And return different result depends on them - * - * @param string $valueName Name of value to check - * @param array $casesResults Cases and results -@@ -3163,6 +3176,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Generate fragment of SQL, that combine together (concatenate) the results from data array -+ * - * All arguments in data must be quoted - * - * @param string[] $data -@@ -3177,6 +3191,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Generate fragment of SQL that returns length of character string -+ * - * The string argument must be quoted - * - * @param string $string -@@ -3371,6 +3386,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Retrieve valid index name -+ * - * Check index name length and allowed symbols - * - * @param string $tableName -@@ -3400,6 +3416,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Retrieve valid foreign key name -+ * - * Check foreign key name length and allowed symbols - * - * @param string $priTableName -@@ -3668,6 +3685,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Adds order by random to select object -+ * - * Possible using integer field for optimization - * - * @param Select $select -@@ -3845,6 +3863,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Parse text size -+ * - * Returns max allowed size if value great it - * - * @param string|int $size -@@ -3879,6 +3898,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface - - /** - * Converts fetched blob into raw binary PHP data. -+ * - * The MySQL drivers do it nice, no processing required. - * - * @param mixed $value --- -2.7.4 - diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch similarity index 100% rename from patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch rename to patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch index 4705e12..dec1608 100644 --- a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch @@ -1,10 +1,6 @@ -Index: vendor/magento/module-sales/Helper/Admin.php -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-sales/Helper/Admin.php (revision 3b69a3b12138bbad688dde2bd89af36aef0cf967) -+++ vendor/magento/module-sales/Helper/Admin.php (revision 1f7017a41bddae5d538eb78e56cbd6374d158503) +diff -Nuar a/vendor/magento/module-sales/Helper/Admin.php b/vendor/magento/module-sales/Helper/Admin.php +--- a/vendor/magento/module-sales/Helper/Admin.php ++++ b/vendor/magento/module-sales/Helper/Admin.php @@ -7,6 +7,8 @@ namespace Magento\Sales\Helper; @@ -122,13 +118,9 @@ Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP $urlScheme = parse_url($url, PHP_URL_SCHEME); $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; if ($urlScheme !== 'http' && $urlScheme !== 'https') { -Index: vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision 3b69a3b12138bbad688dde2bd89af36aef0cf967) -+++ vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php (revision f18c17278c29c594bc729b0da2e6e91150ab6541) +diff -Nuar a/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php b/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php +--- a/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php ++++ b/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php @@ -71,7 +71,7 @@ ->disableOriginalConstructor() ->getMock(); diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch index f7e4612..67bcdd3 100644 --- a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch +++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch @@ -1,13 +1,6 @@ diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php --- a/vendor/magento/framework/App/Router/ActionList.php +++ b/vendor/magento/framework/App/Router/ActionList.php -@@ -1,6 +1,5 @@ - Date: Mon, 2 Dec 2019 17:00:13 -0600 Subject: [PATCH 5/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- patches.json | 10 +++++----- ...ass_store_view_scope_in_async_web_api__2.3.1.patch} | 0 ...ass_store_view_scope_in_async_web_api__2.3.2.patch} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename patches/{MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch => MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch} (100%) rename patches/{MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch => MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch} (100%) diff --git a/patches.json b/patches.json index d0f7ccb..3754361 100644 --- a/patches.json +++ b/patches.json @@ -211,13 +211,13 @@ "2.3.0 - 2.3.1": "PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch" }, "Pass Store View scope in the Async/Bulk Web API": { - "2.3.1": "MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch", - "2.3.2": "MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch" + "2.3.1": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch", + "2.3.2": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch" }, "Admin path disclosure bug": { - ">=2.1.4 <2.2.0": "PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch", - ">=2.2.0 <2.3.0": "PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch", - "2.3.0 - 2.3.2": "PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch" + "2.1.4 - 2.1.17": "PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch", + "2.2.0 - 2.2.8": "PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch", + "2.3.0 - 2.3.1": "PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch" }, "Transaction MD5 hash field is removed by Authorize.net": { "2.2.0 - 2.2.7": "MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch" diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch similarity index 100% rename from patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.1.patch rename to patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch similarity index 100% rename from patches/MAGETWO-99902__pass_store_view_scope_in async_web_api__2.3.2.patch rename to patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch From 0304b0dc5aa45f138dd7d8f8ac94822ff1b7e37d Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Tue, 3 Dec 2019 08:42:47 -0600 Subject: [PATCH 6/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- ...91__customer_attributes_issue__2.2.6.patch | 157 ------- ...e_view_scope_in_async_web_api__2.3.1.patch | 388 ------------------ ...e_view_scope_in_async_web_api__2.3.2.patch | 382 ----------------- 3 files changed, 927 deletions(-) diff --git a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch index 8d9ff62..aac88e8 100644 --- a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch +++ b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch @@ -1237,164 +1237,7 @@ diff -Nuar a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/Swatch + return json_encode($options); } } -diff -Nuar a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php ---- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php -+++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php -@@ -282,13 +282,15 @@ public function testLargeOptionsDataSet() - $optionsData = []; - $expectedOptionsLabels = []; - for ($i = 0; $i < $optionsCount; $i++) { -- $order = $i + 1; -- $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; -+ $expectedOptionLabelOnStoreView = 'value_' . $i . '_store_1'; - $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; -- $optionsData []= "option[order][option_{$i}]={$order}"; -- $optionsData []= "option[value][option_{$i}][0]=value_{$i}_admin"; -- $optionsData []= "option[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; -- $optionsData []= "option[delete][option_{$i}="; -+ $optionId = 'option_' . $i; -+ $optionRowData = []; -+ $optionRowData['option']['order'][$optionId] = $i + 1; -+ $optionRowData['option']['value'][$optionId][0] = 'value_' . $i . '_admin'; -+ $optionRowData['option']['value'][$optionId][1] = $expectedOptionLabelOnStoreView; -+ $optionRowData['option']['delete'][$optionId] = ''; -+ $optionsData[] = http_build_query($optionRowData); - } - $attributeData['serialized_options'] = json_encode($optionsData); - $this->getRequest()->setPostValue($attributeData); -diff -Nuar a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php ---- a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php -+++ b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php -@@ -7,6 +7,8 @@ - - namespace Magento\Swatches\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Request\Http as HttpRequest; -+use Magento\Framework\Data\Form\FormKey; - use Magento\Framework\Exception\LocalizedException; - - /** -@@ -17,6 +19,21 @@ - */ - class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController - { -+ /** -+ * @var FormKey -+ */ -+ private $formKey; -+ -+ /** -+ * @inheritDoc -+ */ -+ protected function setUp() -+ { -+ parent::setUp(); -+ -+ $this->formKey = $this->_objectManager->get(FormKey::class); -+ } -+ - /** - * Generate random hex color. - * -@@ -38,22 +55,27 @@ private function getSwatchVisualDataSet(int $optionsCount): array - $optionsData = []; - $expectedOptionsLabels = []; - for ($i = 0; $i < $optionsCount; $i++) { -- $order = $i + 1; -- $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; -+ $expectedOptionLabelOnStoreView = 'value_' . $i .'_store_1'; - $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; -- $optionsData []= "optionvisual[order][option_{$i}]={$order}"; -- $optionsData []= "defaultvisual[]=option_{$i}"; -- $optionsData []= "swatchvisual[value][option_{$i}]={$this->getRandomColor()}"; -- $optionsData []= "optionvisual[value][option_{$i}][0]=value_{$i}_admin"; -- $optionsData []= "optionvisual[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; -- $optionsData []= "optionvisual[delete][option_{$i}]="; -+ $optionId = 'option_' .$i; -+ $optionRowData = []; -+ $optionRowData['optionvisual']['order'][$optionId] = $i + 1; -+ $optionRowData['defaultvisual'][] = $optionId; -+ $optionRowData['swatchvisual']['value'][$optionId] = $this->getRandomColor(); -+ $optionRowData['optionvisual']['value'][$optionId][0] = 'value_' . $i .'_admin'; -+ $optionRowData['optionvisual']['value'][$optionId][1] = $expectedOptionLabelOnStoreView; -+ $optionRowData['optionvisual']['delete'][$optionId] = ''; -+ $optionsData[] = http_build_query($optionRowData); - } -- $optionsData []= "visual_swatch_validation="; -- $optionsData []= "visual_swatch_validation_unique="; -+ - return [ - 'attribute_data' => array_merge_recursive( - [ -- 'serialized_swatch_values' => json_encode($optionsData), -+ 'serialized_options' => json_encode($optionsData), -+ ], -+ [ -+ 'visual_swatch_validation' => '', -+ 'visual_swatch_validation_unique' => '', - ], - $this->getAttributePreset(), - [ -@@ -76,22 +98,27 @@ private function getSwatchTextDataSet(int $optionsCount): array - $optionsData = []; - $expectedOptionsLabels = []; - for ($i = 0; $i < $optionsCount; $i++) { -- $order = $i + 1; -- $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; -+ $expectedOptionLabelOnStoreView = 'value_' . $i . '_store_1'; - $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; -- $optionsData []= "optiontext[order][option_{$i}]={$order}"; -- $optionsData []= "defaulttext[]=option_{$i}"; -- $optionsData []= "swatchtext[value][option_{$i}]=x{$i}"; -- $optionsData []= "optiontext[value][option_{$i}][0]=value_{$i}_admin"; -- $optionsData []= "optiontext[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; -- $optionsData []= "optiontext[delete][option_{$i}]="; -+ $optionId = 'option_' . $i; -+ $optionRowData = []; -+ $optionRowData['optiontext']['order'][$optionId] = $i + 1; -+ $optionRowData['defaulttext'][] = $optionId; -+ $optionRowData['swatchtext']['value'][$optionId] = 'x' . $i ; -+ $optionRowData['optiontext']['value'][$optionId][0] = 'value_' . $i . '_admin'; -+ $optionRowData['optiontext']['value'][$optionId][1]= $expectedOptionLabelOnStoreView; -+ $optionRowData['optiontext']['delete'][$optionId]=''; -+ $optionsData[] = http_build_query($optionRowData); - } -- $optionsData []= "text_swatch_validation="; -- $optionsData []= "text_swatch_validation_unique="; -+ - return [ - 'attribute_data' => array_merge_recursive( - [ -- 'serialized_swatch_values' => json_encode($optionsData), -+ 'serialized_options' => json_encode($optionsData), -+ ], -+ [ -+ 'text_swatch_validation' => '', -+ 'text_swatch_validation_unique' => '', - ], - $this->getAttributePreset(), - [ -@@ -111,7 +138,6 @@ private function getSwatchTextDataSet(int $optionsCount): array - private function getAttributePreset(): array - { - return [ -- 'serialized_options' => '[]', - 'form_key' => 'XxtpPYjm2YPYUlAt', - 'frontend_label' => [ - 0 => 'asdasd', -@@ -176,7 +202,9 @@ public function testLargeOptionsDataSet( - int $expectedOptionsCount, - array $expectedLabels - ) { -+ $this->getRequest()->setMethod(HttpRequest::METHOD_POST); - $this->getRequest()->setPostValue($attributeData); -+ $this->getRequest()->setPostValue('form_key', $this->formKey->getFormKey()); - $this->dispatch('backend/catalog/product_attribute/save'); - $entityTypeId = $this->_objectManager->create( - \Magento\Eav\Model\Entity::class diff -Nuar a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor/magento/framework/Serialize/Serializer/FormData.php -new file mode 100644 -index 000000000000..077e91797d2b --- /dev/null +++ b/vendor/magento/framework/Serialize/Serializer/FormData.php @@ -0,0 +1,55 @@ diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch index 324ac3f..feebf9d 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch @@ -1,6 +1,4 @@ diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php -new file mode 100644 -index 00000000000..c9899663487 --- /dev/null +++ b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php @@ -0,0 +1,102 @@ @@ -107,8 +105,6 @@ index 00000000000..c9899663487 + } +} diff -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php -new file mode 100644 -index 00000000000..9c54c8485db --- /dev/null +++ b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php @@ -0,0 +1,115 @@ @@ -403,8 +399,6 @@ diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.ph } } diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php -new file mode 100644 -index 00000000000..1327fcc2672 --- /dev/null +++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php @@ -0,0 +1,138 @@ @@ -546,385 +540,3 @@ index 00000000000..1327fcc2672 + return $this->queue; + } +} -diff -Nuar a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php -new file mode 100644 -index 00000000000..61ec73990c9 ---- /dev/null -+++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php -@@ -0,0 +1,374 @@ -+objectManager = Bootstrap::getObjectManager(); -+ $this->logFilePath = TESTS_TEMP_DIR . "/MessageQueueTestLog.txt"; -+ $this->registry = $this->objectManager->get(Registry::class); -+ -+ $params = array_merge_recursive( -+ Bootstrap::getInstance()->getAppInitParams(), -+ ['MAGE_DIRS' => ['cache' => ['path' => TESTS_TEMP_DIR . '/cache']]] -+ ); -+ -+ /** @var PublisherConsumerController publisherConsumerController */ -+ $this->publisherConsumerController = $this->objectManager->create( -+ PublisherConsumerController::class, -+ [ -+ 'consumers' => $this->consumers, -+ 'logFilePath' => $this->logFilePath, -+ 'appInitParams' => $params, -+ ] -+ ); -+ $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); -+ -+ try { -+ $this->publisherConsumerController->initialize(); -+ } catch (EnvironmentPreconditionException $e) { -+ $this->markTestSkipped($e->getMessage()); -+ } catch (PreconditionFailedException $e) { -+ $this->fail( -+ $e->getMessage() -+ ); -+ } -+ -+ parent::setUp(); -+ } -+ -+ /** -+ * @param string $storeCode -+ * @return void -+ * -+ * @dataProvider storeProvider -+ * @magentoApiDataFixture Magento/Store/_files/core_fixturestore.php -+ */ -+ public function testAsyncScheduleBulkMultistore(string $storeCode): void -+ { -+ $product = $this->getProductData(); -+ $this->_markTestAsRestOnly(); -+ -+ /** @var Store $store */ -+ $store = $this->objectManager->create(Store::class); -+ $store->load(self::STORE_CODE_FROM_FIXTURE); -+ $this->assertEquals( -+ self::STORE_NAME_FROM_FIXTURE, -+ $store->getName(), -+ 'Precondition failed: fixture store was not created.' -+ ); -+ -+ try { -+ /** @var Product $productModel */ -+ $productModel = $this->objectManager->create( -+ Product::class, -+ ['data' => $product['product']] -+ ); -+ $this->productRepository->save($productModel); -+ } catch (\Exception $e) { -+ $this->fail("Precondition failed: product was not created."); -+ } -+ -+ $this->asyncScheduleAndTest($product, $storeCode); -+ $this->clearProducts(); -+ } -+ -+ /** -+ * @param array $product -+ * @param string|null $storeCode -+ */ -+ private function asyncScheduleAndTest(array $product, string $storeCode = null): void -+ { -+ $sku = $product['product'][Product::SKU]; -+ $productName = $product['product'][Product::NAME]; -+ $newProductName = $product['product'][Product::NAME] . $storeCode; -+ -+ $this->skus[] = $sku; -+ -+ $product['product'][Product::NAME] = $newProductName; -+ $product['product'][Product::TYPE_ID] = 'virtual'; -+ -+ $response = $this->updateProductAsync($product, $sku, $storeCode); -+ -+ $this->assertArrayHasKey(self::BULK_UUID_KEY, $response); -+ $this->assertNotNull($response[self::BULK_UUID_KEY]); -+ -+ $this->assertCount(1, $response['request_items']); -+ $this->assertEquals('accepted', $response['request_items'][0]['status']); -+ $this->assertFalse($response['errors']); -+ -+ //assert product is updated -+ try { -+ $this->publisherConsumerController->waitForAsynchronousResult( -+ [$this, 'assertProductCreation'], -+ [$product] -+ ); -+ } catch (PreconditionFailedException $e) { -+ $this->fail("Product is not updated"); -+ } -+ -+ $requestData = ['id' => $sku, 'sku' => $sku]; -+ -+ foreach ($this->stores as $checkingStore) { -+ $serviceInfo = [ -+ 'rest' => [ -+ 'resourcePath' => self::REST_RESOURCE_PATH . '/' . $sku, -+ 'httpMethod' => Request::HTTP_METHOD_GET, -+ ] -+ ]; -+ $storeResponse = $this->_webApiCall($serviceInfo, $requestData, null, $checkingStore); -+ if ($checkingStore == $storeCode || $storeCode == self::STORE_CODE_ALL) { -+ $this->assertEquals( -+ $newProductName, -+ $storeResponse[Product::NAME], -+ sprintf( -+ 'Product name in %s store is invalid after updating in store %s.', -+ $checkingStore, -+ $storeCode -+ ) -+ ); -+ } else { -+ $this->assertEquals( -+ $productName, -+ $storeResponse[Product::NAME], -+ sprintf( -+ 'Product name in %s store is invalid after updating in store %s.', -+ $checkingStore, -+ $storeCode -+ ) -+ ); -+ } -+ } -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function tearDown() -+ { -+ $this->clearProducts(); -+ $this->publisherConsumerController->stopConsumers(); -+ parent::tearDown(); -+ } -+ -+ /** -+ * @throws Exception -+ */ -+ private function clearProducts() -+ { -+ /** @var Collection $productCollection */ -+ $productCollection = $this->objectManager->create(Collection::class) -+ ->addAttributeToFilter('sku', ['in' => $this->skus]) -+ ->load(); -+ -+ if ($productCollection->getSize() == 0) { -+ return; -+ } -+ -+ $this->registry->unregister('isSecureArea'); -+ $this->registry->register('isSecureArea', true); -+ try { -+ foreach ($this->skus as $sku) { -+ $this->productRepository->deleteById($sku); -+ } -+ // phpcs:ignore Magento2.Exceptions.ThrowCatch -+ } catch (\Exception $e) { -+ //nothing to delete -+ throw $e; -+ } -+ $this->registry->unregister('isSecureArea'); -+ -+ $size = $productCollection->clear()->getSize(); -+ -+ if ($size > 0) { -+ //phpcs:ignore Magento2.Exceptions.DirectThrow -+ throw new Exception(new Phrase("Collection size after clearing the products: %size", ['size' => $size])); -+ } -+ $this->skus = []; -+ } -+ -+ /** -+ * @return array -+ */ -+ public function getProductData(): array -+ { -+ $productBuilder = function ($data) { -+ return array_replace_recursive( -+ $this->getSimpleProductData(), -+ $data -+ ); -+ }; -+ -+ return [ -+ 'product' => -+ $productBuilder( -+ [ -+ ProductInterface::TYPE_ID => 'simple', -+ ProductInterface::SKU => 'multistore-sku-test-1', -+ ProductInterface::NAME => 'Test Name ', -+ ] -+ ), -+ ]; -+ } -+ -+ /** -+ * @return array -+ */ -+ public function storeProvider(): array -+ { -+ $dataSets = []; -+ foreach ($this->stores as $store) { -+ $dataSets[$store] = [$store]; -+ } -+ -+ return $dataSets; -+ } -+ -+ /** -+ * Get Simple Product Data -+ * -+ * @param array $productData -+ * @return array -+ */ -+ private function getSimpleProductData($productData = []): array -+ { -+ return [ -+ ProductInterface::SKU => isset($productData[ProductInterface::SKU]) -+ ? $productData[ProductInterface::SKU] : uniqid('sku-', true), -+ ProductInterface::NAME => isset($productData[ProductInterface::NAME]) -+ ? $productData[ProductInterface::NAME] : uniqid('sku-', true), -+ ProductInterface::VISIBILITY => 4, -+ ProductInterface::TYPE_ID => 'simple', -+ ProductInterface::PRICE => 3.62, -+ ProductInterface::STATUS => 1, -+ ProductInterface::ATTRIBUTE_SET_ID => 4, -+ ]; -+ } -+ -+ /** -+ * @param array $requestData -+ * @param string $sku -+ * @param string|null $storeCode -+ * @return mixed -+ */ -+ private function updateProductAsync(array $requestData, string $sku, string $storeCode = null) -+ { -+ $serviceInfo = [ -+ 'rest' => [ -+ 'resourcePath' => self::ASYNC_RESOURCE_PATH . '/' . $sku, -+ 'httpMethod' => Request::HTTP_METHOD_PUT, -+ ], -+ ]; -+ -+ return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode); -+ } -+ -+ /** -+ * @param array $product -+ * @return bool -+ */ -+ public function assertProductCreation(array $product): bool -+ { -+ $sku = $product['product'][Product::SKU]; -+ $collection = $this->objectManager->create(Collection::class) -+ ->addAttributeToFilter(Product::SKU, ['eq' => $sku]) -+ ->addAttributeToFilter(Product::TYPE_ID, ['eq' => 'virtual']) -+ ->load(); -+ $size = $collection->getSize(); -+ -+ return $size > 0; -+ } -+} --- -2.17.1 diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch index e015e39..d40874e 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch @@ -560,385 +560,3 @@ index 00000000000..1327fcc2672 + return $this->queue; + } +} -diff -Nuar a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php -new file mode 100644 -index 00000000000..61ec73990c9 ---- /dev/null -+++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php -@@ -0,0 +1,374 @@ -+objectManager = Bootstrap::getObjectManager(); -+ $this->logFilePath = TESTS_TEMP_DIR . "/MessageQueueTestLog.txt"; -+ $this->registry = $this->objectManager->get(Registry::class); -+ -+ $params = array_merge_recursive( -+ Bootstrap::getInstance()->getAppInitParams(), -+ ['MAGE_DIRS' => ['cache' => ['path' => TESTS_TEMP_DIR . '/cache']]] -+ ); -+ -+ /** @var PublisherConsumerController publisherConsumerController */ -+ $this->publisherConsumerController = $this->objectManager->create( -+ PublisherConsumerController::class, -+ [ -+ 'consumers' => $this->consumers, -+ 'logFilePath' => $this->logFilePath, -+ 'appInitParams' => $params, -+ ] -+ ); -+ $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); -+ -+ try { -+ $this->publisherConsumerController->initialize(); -+ } catch (EnvironmentPreconditionException $e) { -+ $this->markTestSkipped($e->getMessage()); -+ } catch (PreconditionFailedException $e) { -+ $this->fail( -+ $e->getMessage() -+ ); -+ } -+ -+ parent::setUp(); -+ } -+ -+ /** -+ * @param string $storeCode -+ * @return void -+ * -+ * @dataProvider storeProvider -+ * @magentoApiDataFixture Magento/Store/_files/core_fixturestore.php -+ */ -+ public function testAsyncScheduleBulkMultistore(string $storeCode): void -+ { -+ $product = $this->getProductData(); -+ $this->_markTestAsRestOnly(); -+ -+ /** @var Store $store */ -+ $store = $this->objectManager->create(Store::class); -+ $store->load(self::STORE_CODE_FROM_FIXTURE); -+ $this->assertEquals( -+ self::STORE_NAME_FROM_FIXTURE, -+ $store->getName(), -+ 'Precondition failed: fixture store was not created.' -+ ); -+ -+ try { -+ /** @var Product $productModel */ -+ $productModel = $this->objectManager->create( -+ Product::class, -+ ['data' => $product['product']] -+ ); -+ $this->productRepository->save($productModel); -+ } catch (\Exception $e) { -+ $this->fail("Precondition failed: product was not created."); -+ } -+ -+ $this->asyncScheduleAndTest($product, $storeCode); -+ $this->clearProducts(); -+ } -+ -+ /** -+ * @param array $product -+ * @param string|null $storeCode -+ */ -+ private function asyncScheduleAndTest(array $product, string $storeCode = null): void -+ { -+ $sku = $product['product'][Product::SKU]; -+ $productName = $product['product'][Product::NAME]; -+ $newProductName = $product['product'][Product::NAME] . $storeCode; -+ -+ $this->skus[] = $sku; -+ -+ $product['product'][Product::NAME] = $newProductName; -+ $product['product'][Product::TYPE_ID] = 'virtual'; -+ -+ $response = $this->updateProductAsync($product, $sku, $storeCode); -+ -+ $this->assertArrayHasKey(self::BULK_UUID_KEY, $response); -+ $this->assertNotNull($response[self::BULK_UUID_KEY]); -+ -+ $this->assertCount(1, $response['request_items']); -+ $this->assertEquals('accepted', $response['request_items'][0]['status']); -+ $this->assertFalse($response['errors']); -+ -+ //assert product is updated -+ try { -+ $this->publisherConsumerController->waitForAsynchronousResult( -+ [$this, 'assertProductCreation'], -+ [$product] -+ ); -+ } catch (PreconditionFailedException $e) { -+ $this->fail("Product is not updated"); -+ } -+ -+ $requestData = ['id' => $sku, 'sku' => $sku]; -+ -+ foreach ($this->stores as $checkingStore) { -+ $serviceInfo = [ -+ 'rest' => [ -+ 'resourcePath' => self::REST_RESOURCE_PATH . '/' . $sku, -+ 'httpMethod' => Request::HTTP_METHOD_GET, -+ ] -+ ]; -+ $storeResponse = $this->_webApiCall($serviceInfo, $requestData, null, $checkingStore); -+ if ($checkingStore == $storeCode || $storeCode == self::STORE_CODE_ALL) { -+ $this->assertEquals( -+ $newProductName, -+ $storeResponse[Product::NAME], -+ sprintf( -+ 'Product name in %s store is invalid after updating in store %s.', -+ $checkingStore, -+ $storeCode -+ ) -+ ); -+ } else { -+ $this->assertEquals( -+ $productName, -+ $storeResponse[Product::NAME], -+ sprintf( -+ 'Product name in %s store is invalid after updating in store %s.', -+ $checkingStore, -+ $storeCode -+ ) -+ ); -+ } -+ } -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function tearDown() -+ { -+ $this->clearProducts(); -+ $this->publisherConsumerController->stopConsumers(); -+ parent::tearDown(); -+ } -+ -+ /** -+ * @throws Exception -+ */ -+ private function clearProducts() -+ { -+ /** @var Collection $productCollection */ -+ $productCollection = $this->objectManager->create(Collection::class) -+ ->addAttributeToFilter('sku', ['in' => $this->skus]) -+ ->load(); -+ -+ if ($productCollection->getSize() == 0) { -+ return; -+ } -+ -+ $this->registry->unregister('isSecureArea'); -+ $this->registry->register('isSecureArea', true); -+ try { -+ foreach ($this->skus as $sku) { -+ $this->productRepository->deleteById($sku); -+ } -+ // phpcs:ignore Magento2.Exceptions.ThrowCatch -+ } catch (\Exception $e) { -+ //nothing to delete -+ throw $e; -+ } -+ $this->registry->unregister('isSecureArea'); -+ -+ $size = $productCollection->clear()->getSize(); -+ -+ if ($size > 0) { -+ //phpcs:ignore Magento2.Exceptions.DirectThrow -+ throw new Exception(new Phrase("Collection size after clearing the products: %size", ['size' => $size])); -+ } -+ $this->skus = []; -+ } -+ -+ /** -+ * @return array -+ */ -+ public function getProductData(): array -+ { -+ $productBuilder = function ($data) { -+ return array_replace_recursive( -+ $this->getSimpleProductData(), -+ $data -+ ); -+ }; -+ -+ return [ -+ 'product' => -+ $productBuilder( -+ [ -+ ProductInterface::TYPE_ID => 'simple', -+ ProductInterface::SKU => 'multistore-sku-test-1', -+ ProductInterface::NAME => 'Test Name ', -+ ] -+ ), -+ ]; -+ } -+ -+ /** -+ * @return array -+ */ -+ public function storeProvider(): array -+ { -+ $dataSets = []; -+ foreach ($this->stores as $store) { -+ $dataSets[$store] = [$store]; -+ } -+ -+ return $dataSets; -+ } -+ -+ /** -+ * Get Simple Product Data -+ * -+ * @param array $productData -+ * @return array -+ */ -+ private function getSimpleProductData($productData = []): array -+ { -+ return [ -+ ProductInterface::SKU => isset($productData[ProductInterface::SKU]) -+ ? $productData[ProductInterface::SKU] : uniqid('sku-', true), -+ ProductInterface::NAME => isset($productData[ProductInterface::NAME]) -+ ? $productData[ProductInterface::NAME] : uniqid('sku-', true), -+ ProductInterface::VISIBILITY => 4, -+ ProductInterface::TYPE_ID => 'simple', -+ ProductInterface::PRICE => 3.62, -+ ProductInterface::STATUS => 1, -+ ProductInterface::ATTRIBUTE_SET_ID => 4, -+ ]; -+ } -+ -+ /** -+ * @param array $requestData -+ * @param string $sku -+ * @param string|null $storeCode -+ * @return mixed -+ */ -+ private function updateProductAsync(array $requestData, string $sku, string $storeCode = null) -+ { -+ $serviceInfo = [ -+ 'rest' => [ -+ 'resourcePath' => self::ASYNC_RESOURCE_PATH . '/' . $sku, -+ 'httpMethod' => Request::HTTP_METHOD_PUT, -+ ], -+ ]; -+ -+ return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode); -+ } -+ -+ /** -+ * @param array $product -+ * @return bool -+ */ -+ public function assertProductCreation(array $product): bool -+ { -+ $sku = $product['product'][Product::SKU]; -+ $collection = $this->objectManager->create(Collection::class) -+ ->addAttributeToFilter(Product::SKU, ['eq' => $sku]) -+ ->addAttributeToFilter(Product::TYPE_ID, ['eq' => 'virtual']) -+ ->load(); -+ $size = $collection->getSize(); -+ -+ return $size > 0; -+ } -+} --- -2.17.1 From ed1cba4d2b8007755038d7aef00268adb46f5bff Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Tue, 3 Dec 2019 09:20:53 -0600 Subject: [PATCH 7/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- patches.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches.json b/patches.json index 1ddce0a..1845de5 100644 --- a/patches.json +++ b/patches.json @@ -212,7 +212,7 @@ }, "Pass Store View scope in the Async/Bulk Web API": { "2.3.1": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch", - "2.3.2": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch" + ">=2.3.2 <2.3.3": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch" }, "Admin path disclosure bug": { "2.1.4 - 2.1.17": "PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch", From 28ae1ee1eed97d019c2a96a6ab0beb30bc0fb75a Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Tue, 3 Dec 2019 10:12:24 -0600 Subject: [PATCH 8/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- .../MAGETWO-95591__customer_attributes_issue__2.2.6.patch | 2 -- ...902__pass_store_view_scope_in_async_web_api__2.3.2.patch | 6 ------ 2 files changed, 8 deletions(-) diff --git a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch index aac88e8..4f963b8 100644 --- a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch +++ b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch @@ -1297,8 +1297,6 @@ diff -Nuar a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor + } +} diff -Nuar a/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php -new file mode 100644 -index 000000000000..7729a2da97ef --- /dev/null +++ b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php @@ -0,0 +1,106 @@ diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch index d40874e..99257df 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch @@ -1,6 +1,4 @@ diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php -new file mode 100644 -index 00000000000..c9899663487 --- /dev/null +++ b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php @@ -0,0 +1,102 @@ @@ -107,8 +105,6 @@ index 00000000000..c9899663487 + } +} diff -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php -new file mode 100644 -index 00000000000..9c54c8485db --- /dev/null +++ b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php @@ -0,0 +1,115 @@ @@ -417,8 +413,6 @@ diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.ph } } diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php -new file mode 100644 -index 00000000000..1327fcc2672 --- /dev/null +++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php @@ -0,0 +1,138 @@ From d962c3393c58eb4aabb4d8940e961edda0dd0e57 Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Wed, 4 Dec 2019 15:43:29 -0600 Subject: [PATCH 9/9] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ --- patches.json | 1 + ...233__fix_xss_in_order_history__2.2.0.patch | 146 ++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch diff --git a/patches.json b/patches.json index 1845de5..09e3449 100644 --- a/patches.json +++ b/patches.json @@ -207,6 +207,7 @@ "2.3.2": "MDVA-22979__fix_pagebuilder_module__2.3.2.patch" }, "Fix XSS in order history": { + "2.2.0 - 2.2.6": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch", "2.2.7 - 2.2.8": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch", "2.3.0 - 2.3.1": "PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch" }, diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch new file mode 100644 index 0000000..ce71620 --- /dev/null +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch @@ -0,0 +1,146 @@ +diff -Nuar a/vendor/magento/module-sales/Helper/Admin.php b/vendor/magento/module-sales/Helper/Admin.php +--- a/vendor/magento/module-sales/Helper/Admin.php ++++ b/vendor/magento/module-sales/Helper/Admin.php +@@ -3,8 +3,14 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Sales\Helper; + ++use Magento\Framework\App\ObjectManager; ++ ++/** ++ * Sales admin helper. ++ */ + class Admin extends \Magento\Framework\App\Helper\AbstractHelper + { + /** +@@ -27,24 +33,33 @@ class Admin extends \Magento\Framework\App\Helper\AbstractHelper + */ + protected $escaper; + ++ /** ++ * @var \DOMDocumentFactory ++ */ ++ private $domDocumentFactory; ++ + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Sales\Model\Config $salesConfig + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Framework\Escaper $escaper ++ * @param \DOMDocumentFactory|null $domDocumentFactory + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Sales\Model\Config $salesConfig, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, +- \Magento\Framework\Escaper $escaper ++ \Magento\Framework\Escaper $escaper, ++ \DOMDocumentFactory $domDocumentFactory = null + ) { + $this->priceCurrency = $priceCurrency; + $this->_storeManager = $storeManager; + $this->_salesConfig = $salesConfig; + $this->escaper = $escaper; ++ $this->domDocumentFactory = $domDocumentFactory ++ ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); + parent::__construct($context); + } + +@@ -145,37 +160,65 @@ class Admin extends \Magento\Framework\App\Helper\AbstractHelper + public function escapeHtmlWithLinks($data, $allowedTags = null) + { + if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { +- $links = []; +- $i = 1; +- $data = str_replace('%', '%%', $data); +- $regexp = "/]*href\s*?=\s*?([\"\']??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU"; +- while (preg_match($regexp, $data, $matches)) { +- //Revert the sprintf escaping +- $url = str_replace('%%', '%', $matches[2]); +- $text = str_replace('%%', '%', $matches[3]); +- //Check for an valid url +- if ($url) { +- $urlScheme = strtolower(parse_url($url, PHP_URL_SCHEME)); +- if ($urlScheme !== 'http' && $urlScheme !== 'https') { +- $url = null; +- } ++ $wrapperElementId = uniqid(); ++ $domDocument = $this->domDocumentFactory->create(); ++ ++ $internalErrors = libxml_use_internal_errors(true); ++ ++ $domDocument->loadHTML( ++ '' . $data . '' ++ ); ++ ++ libxml_use_internal_errors($internalErrors); ++ ++ $linkTags = $domDocument->getElementsByTagName('a'); ++ ++ foreach ($linkTags as $linkNode) { ++ $linkAttributes = []; ++ foreach ($linkNode->attributes as $attribute) { ++ $linkAttributes[$attribute->name] = $attribute->value; + } +- //Use hash tag as fallback +- if (!$url) { +- $url = '#'; ++ ++ foreach ($linkAttributes as $attributeName => $attributeValue) { ++ if ($attributeName === 'href') { ++ $url = $this->filterUrl($attributeValue ?? ''); ++ $url = $this->escaper->escapeUrl($url); ++ $linkNode->setAttribute('href', $url); ++ } else { ++ $linkNode->removeAttribute($attributeName); ++ } + } +- //Recreate a minimalistic secure a tag +- $links[] = sprintf( +- '%s', +- htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), +- $this->escaper->escapeHtml($text) +- ); +- $data = str_replace($matches[0], '%' . $i . '$s', $data); +- ++$i; + } +- $data = $this->escaper->escapeHtml($data, $allowedTags); +- return vsprintf($data, $links); ++ ++ $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); ++ preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); ++ $data = !empty($matches) ? $matches[1] : ''; + } ++ + return $this->escaper->escapeHtml($data, $allowedTags); + } ++ ++ /** ++ * Filter the URL for allowed protocols. ++ * ++ * @param string $url ++ * @return string ++ */ ++ private function filterUrl(string $url): string ++ { ++ if ($url) { ++ //Revert the sprintf escaping ++ $urlScheme = parse_url($url, PHP_URL_SCHEME); ++ $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; ++ if ($urlScheme !== 'http' && $urlScheme !== 'https') { ++ $url = null; ++ } ++ } ++ ++ if (!$url) { ++ $url = '#'; ++ } ++ ++ return $url; ++ } + }