').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 -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 @@
+ 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 -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([
+ '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 -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)
+ $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 -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
+ ];
+ }
+
++ /**
++ * @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 -Nuar a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor/magento/framework/Serialize/Serializer/FormData.php
+--- /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 -Nuar a/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php
+--- /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 -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 @@
+ 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..be518ee
--- /dev/null
+++ b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch
@@ -0,0 +1,322 @@
+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()
+ {
+ $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;
+ }
+
+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;
+
+ 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;
++ }
+ }
+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)
+ {
+- 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)));
++ }
+ }
+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
+
++
++
++ Magento\Config\Model\Config\Backend\Encrypted
++
+
+
+ Magento\Config\Model\Config\Backend\Encrypted
+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)
+
+
++
+ 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..feebf9d
--- /dev/null
+++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch
@@ -0,0 +1,542 @@
+diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php
+--- /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 -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php
+--- /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 -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 @@
+ "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 -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 @@
+ \Magento\Framework\Amqp\Bulk\Exchange
+
+
++
++
++
++
++
++
+
+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 @@
+ */
+ -->
+
+-
++
++
++
++
++
+
+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);
+
+ 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 -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php
+--- /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/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..99257df
--- /dev/null
+++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch
@@ -0,0 +1,556 @@
+diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php
+--- /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 -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php
+--- /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 -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 @@
+ "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 -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 @@
+ \Magento\Framework\Amqp\Bulk\Exchange
+
+
++
++
++
++
++
++
+
+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 @@
+ */
+ -->
+
+-
++
++
++
++
++
+
+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);
+
+ 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 -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php
+--- /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/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;
++ }
+ }
diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch
new file mode 100644
index 0000000..a367b77
--- /dev/null
+++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch
@@ -0,0 +1,119 @@
+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;
+
++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') {
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..dec1608
--- /dev/null
+++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch
@@ -0,0 +1,209 @@
+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;
+
++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') {
+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();
+
+- $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..67bcdd3
--- /dev/null
+++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch
@@ -0,0 +1,21 @@
+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
+@@ -8,6 +7,9 @@ namespace Magento\Framework\App\Router;
+
+ use Magento\Framework\Module\Dir\Reader as ModuleReader;
+
++/**
++ * Class to retrieve action class.
++ */
+ class ActionList
+ {
+ /**
+@@ -74,6 +76,7 @@ class ActionList
+ if ($area) {
+ $area = '\\' . $area;
+ }
++ $namespace = strtolower($namespace);
+ if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) {
+ return null;
+ }
diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch
new file mode 100644
index 0000000..c2b230d
--- /dev/null
+++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch
@@ -0,0 +1,22 @@
+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
+@@ -10,6 +9,9 @@ use Magento\Framework\Serialize\SerializerInterface;
+ use Magento\Framework\Serialize\Serializer\Serialize;
+ use Magento\Framework\Module\Dir\Reader as ModuleReader;
+
++/**
++ * Class to retrieve action class.
++ */
+ class ActionList
+ {
+ /**
+@@ -91,6 +93,7 @@ class ActionList
+ if ($area) {
+ $area = '\\' . $area;
+ }
++ $namespace = strtolower($namespace);
+ if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) {
+ return null;
+ }
+
diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch
new file mode 100644
index 0000000..c2b230d
--- /dev/null
+++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch
@@ -0,0 +1,22 @@
+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
+@@ -10,6 +9,9 @@ use Magento\Framework\Serialize\SerializerInterface;
+ use Magento\Framework\Serialize\Serializer\Serialize;
+ use Magento\Framework\Module\Dir\Reader as ModuleReader;
+
++/**
++ * Class to retrieve action class.
++ */
+ class ActionList
+ {
+ /**
+@@ -91,6 +93,7 @@ class ActionList
+ if ($area) {
+ $area = '\\' . $area;
+ }
++ $namespace = strtolower($namespace);
+ if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) {
+ return null;
+ }
+