From 5972daa50f8ecae9fa25f19df083757774737529 Mon Sep 17 00:00:00 2001 From: Anton Evers Date: Thu, 2 Jul 2020 18:02:08 +0200 Subject: [PATCH 001/137] WIP Scheduled price rule time zone correction --- .../Model/Indexer/IndexBuilder.php | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 1fc53c78985fb..6ea99419366d4 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -6,6 +6,7 @@ namespace Magento\CatalogRule\Model\Indexer; +use DateTime; use Magento\Catalog\Model\Product; use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; @@ -14,6 +15,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\ScopeInterface; /** * Catalog rule index builder @@ -150,6 +153,11 @@ class IndexBuilder */ private $productLoader; + /** + * @var TimezoneInterface|mixed + */ + private $localeDate; + /** * @param RuleCollectionFactory $ruleCollectionFactory * @param PriceCurrencyInterface $priceCurrency @@ -170,6 +178,7 @@ class IndexBuilder * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher * @param ProductLoader|null $productLoader * @param TableSwapper|null $tableSwapper + * @param TimezoneInterface|null $localeDate * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -191,7 +200,8 @@ public function __construct( RuleProductPricesPersistor $pricesPersistor = null, \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, ProductLoader $productLoader = null, - TableSwapper $tableSwapper = null + TableSwapper $tableSwapper = null, + ?TimezoneInterface $localeDate = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -231,6 +241,8 @@ public function __construct( ); $this->tableSwapper = $tableSwapper ?? ObjectManager::getInstance()->get(TableSwapper::class); + $this->localeDate = $localeDate ?? + ObjectManager::getInstance()->get(TimezoneInterface::class); } /** @@ -389,7 +401,9 @@ protected function cleanByIds($productIds) * @param Rule $rule * @param int $productEntityId * @param array $websiteIds + * * @return void + * @throws \Exception */ private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void { @@ -404,9 +418,6 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we ); $customerGroupIds = $rule->getCustomerGroupIds(); - $fromTime = strtotime($rule->getFromDate()); - $toTime = strtotime($rule->getToDate()); - $toTime = $toTime ? $toTime + self::SECONDS_IN_DAY - 1 : 0; $sortOrder = (int)$rule->getSortOrder(); $actionOperator = $rule->getSimpleAction(); $actionAmount = $rule->getDiscountAmount(); @@ -414,6 +425,16 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we $rows = []; foreach ($websiteIds as $websiteId) { + $scopeTz = new \DateTimeZone( + $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) + ); + $fromTime = $rule->getFromDate() + ? (new DateTime($rule->getFromDate(), $scopeTz))->getTimestamp() + : 0; + $toTime = $rule->getToDate() + ? (new DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 + : 0; + foreach ($customerGroupIds as $customerGroupId) { $rows[] = [ 'rule_id' => $ruleId, From 0e68e7082d94098a081fd9c0c5cb98d7738dc4e2 Mon Sep 17 00:00:00 2001 From: Anton Evers Date: Fri, 21 Aug 2020 16:46:49 +0200 Subject: [PATCH 002/137] reindexById and reindexByIds should be functionally equal --- .../CatalogRule/Model/Indexer/IndexBuilder.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 6ea99419366d4..ada78212434ba 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -255,19 +255,11 @@ public function __construct( public function reindexById($id) { try { - $this->cleanProductIndex([$id]); - - $products = $this->productLoader->getProducts([$id]); - $activeRules = $this->getActiveRules(); - foreach ($products as $product) { - $this->applyRules($activeRules, $product); - } - - $this->reindexRuleGroupWebsite->execute(); + $this->doReindexByIds([$id]); } catch (\Exception $e) { $this->critical($e); throw new \Magento\Framework\Exception\LocalizedException( - __('Catalog rule indexing failed. See details in exception log.') + __("Catalog rule indexing failed. See details in exception log.") ); } } @@ -404,6 +396,8 @@ protected function cleanByIds($productIds) * * @return void * @throws \Exception + * @deprecated + * @see ReindexRuleProduct::execute */ private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void { @@ -488,6 +482,8 @@ protected function applyRule(Rule $rule, $product) * * @param RuleCollection $ruleCollection * @param Product $product + * @deprecated + * @see ReindexRuleProduct::execute * @return void */ private function applyRules(RuleCollection $ruleCollection, Product $product): void From c7def247f2c8014f0b8b2800a861bb616e23b34c Mon Sep 17 00:00:00 2001 From: Anton Evers Date: Fri, 21 Aug 2020 16:48:25 +0200 Subject: [PATCH 003/137] Revert "WIP Scheduled price rule time zone correction" This reverts commit 5972daa50f8ecae9fa25f19df083757774737529. --- .../Model/Indexer/IndexBuilder.php | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index ada78212434ba..d3bc004a254f5 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -6,7 +6,6 @@ namespace Magento\CatalogRule\Model\Indexer; -use DateTime; use Magento\Catalog\Model\Product; use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; @@ -15,8 +14,6 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; -use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -use Magento\Store\Model\ScopeInterface; /** * Catalog rule index builder @@ -153,11 +150,6 @@ class IndexBuilder */ private $productLoader; - /** - * @var TimezoneInterface|mixed - */ - private $localeDate; - /** * @param RuleCollectionFactory $ruleCollectionFactory * @param PriceCurrencyInterface $priceCurrency @@ -178,7 +170,6 @@ class IndexBuilder * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher * @param ProductLoader|null $productLoader * @param TableSwapper|null $tableSwapper - * @param TimezoneInterface|null $localeDate * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -200,8 +191,7 @@ public function __construct( RuleProductPricesPersistor $pricesPersistor = null, \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, ProductLoader $productLoader = null, - TableSwapper $tableSwapper = null, - ?TimezoneInterface $localeDate = null + TableSwapper $tableSwapper = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -241,8 +231,6 @@ public function __construct( ); $this->tableSwapper = $tableSwapper ?? ObjectManager::getInstance()->get(TableSwapper::class); - $this->localeDate = $localeDate ?? - ObjectManager::getInstance()->get(TimezoneInterface::class); } /** @@ -393,7 +381,6 @@ protected function cleanByIds($productIds) * @param Rule $rule * @param int $productEntityId * @param array $websiteIds - * * @return void * @throws \Exception * @deprecated @@ -412,6 +399,9 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we ); $customerGroupIds = $rule->getCustomerGroupIds(); + $fromTime = strtotime($rule->getFromDate()); + $toTime = strtotime($rule->getToDate()); + $toTime = $toTime ? $toTime + self::SECONDS_IN_DAY - 1 : 0; $sortOrder = (int)$rule->getSortOrder(); $actionOperator = $rule->getSimpleAction(); $actionAmount = $rule->getDiscountAmount(); @@ -419,16 +409,6 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we $rows = []; foreach ($websiteIds as $websiteId) { - $scopeTz = new \DateTimeZone( - $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) - ); - $fromTime = $rule->getFromDate() - ? (new DateTime($rule->getFromDate(), $scopeTz))->getTimestamp() - : 0; - $toTime = $rule->getToDate() - ? (new DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 - : 0; - foreach ($customerGroupIds as $customerGroupId) { $rows[] = [ 'rule_id' => $ruleId, From c614f893c6792fa4883b2e86845e991ccf987175 Mon Sep 17 00:00:00 2001 From: Anton Evers Date: Fri, 21 Aug 2020 16:56:16 +0200 Subject: [PATCH 004/137] remove unused code --- .../Model/Indexer/IndexBuilder.php | 167 ++++-------------- 1 file changed, 39 insertions(+), 128 deletions(-) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index d3bc004a254f5..84397562ead60 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -7,13 +7,21 @@ namespace Magento\CatalogRule\Model\Indexer; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule; +use Magento\Eav\Model\Config; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * Catalog rule index builder @@ -46,12 +54,12 @@ class IndexBuilder protected $_catalogRuleGroupWebsiteColumnsList = ['rule_id', 'customer_group_id', 'website_id']; /** - * @var \Magento\Framework\App\ResourceConnection + * @var ResourceConnection */ protected $resource; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; @@ -61,7 +69,7 @@ class IndexBuilder protected $ruleCollectionFactory; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; @@ -71,22 +79,22 @@ class IndexBuilder protected $priceCurrency; /** - * @var \Magento\Eav\Model\Config + * @var Config */ protected $eavConfig; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $dateFormat; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime + * @var DateTime\DateTime */ protected $dateTime; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $productFactory; @@ -135,31 +143,21 @@ class IndexBuilder */ private $pricesPersistor; - /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher - */ - private $activeTableSwitcher; - /** * @var TableSwapper */ private $tableSwapper; - /** - * @var ProductLoader - */ - private $productLoader; - /** * @param RuleCollectionFactory $ruleCollectionFactory * @param PriceCurrencyInterface $priceCurrency - * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\Stdlib\DateTime $dateFormat - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime - * @param \Magento\Catalog\Model\ProductFactory $productFactory + * @param ResourceConnection $resource + * @param StoreManagerInterface $storeManager + * @param LoggerInterface $logger + * @param Config $eavConfig + * @param DateTime $dateFormat + * @param DateTime\DateTime $dateTime + * @param ProductFactory $productFactory * @param int $batchCount * @param ProductPriceCalculator|null $productPriceCalculator * @param ReindexRuleProduct|null $reindexRuleProduct @@ -167,21 +165,22 @@ class IndexBuilder * @param RuleProductsSelectBuilder|null $ruleProductsSelectBuilder * @param ReindexRuleProductPrice|null $reindexRuleProductPrice * @param RuleProductPricesPersistor|null $pricesPersistor - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher + * @param ActiveTableSwitcher|null $activeTableSwitcher * @param ProductLoader|null $productLoader * @param TableSwapper|null $tableSwapper * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( RuleCollectionFactory $ruleCollectionFactory, PriceCurrencyInterface $priceCurrency, - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Psr\Log\LoggerInterface $logger, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\Stdlib\DateTime $dateFormat, - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - \Magento\Catalog\Model\ProductFactory $productFactory, + ResourceConnection $resource, + StoreManagerInterface $storeManager, + LoggerInterface $logger, + Config $eavConfig, + DateTime $dateFormat, + DateTime\DateTime $dateTime, + ProductFactory $productFactory, $batchCount = 1000, ProductPriceCalculator $productPriceCalculator = null, ReindexRuleProduct $reindexRuleProduct = null, @@ -189,7 +188,7 @@ public function __construct( RuleProductsSelectBuilder $ruleProductsSelectBuilder = null, ReindexRuleProductPrice $reindexRuleProductPrice = null, RuleProductPricesPersistor $pricesPersistor = null, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, + ActiveTableSwitcher $activeTableSwitcher = null, ProductLoader $productLoader = null, TableSwapper $tableSwapper = null ) { @@ -223,12 +222,6 @@ public function __construct( $this->pricesPersistor = $pricesPersistor ?? ObjectManager::getInstance()->get( RuleProductPricesPersistor::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?? ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class - ); - $this->productLoader = $productLoader ?? ObjectManager::getInstance()->get( - ProductLoader::class - ); $this->tableSwapper = $tableSwapper ?? ObjectManager::getInstance()->get(TableSwapper::class); } @@ -237,6 +230,7 @@ public function __construct( * Reindex by id * * @param int $id + * @throws LocalizedException * @return void * @api */ @@ -246,7 +240,7 @@ public function reindexById($id) $this->doReindexByIds([$id]); } catch (\Exception $e) { $this->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Catalog rule indexing failed. See details in exception log.") ); } @@ -256,7 +250,7 @@ public function reindexById($id) * Reindex by ids * * @param array $ids - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return void * @api */ @@ -266,7 +260,7 @@ public function reindexByIds(array $ids) $this->doReindexByIds($ids); } catch (\Exception $e) { $this->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Catalog rule indexing failed. See details in exception log.") ); } @@ -300,7 +294,7 @@ protected function doReindexByIds($ids) /** * Full reindex * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return void * @api */ @@ -310,7 +304,7 @@ public function reindexFull() $this->doReindexFull(); } catch (\Exception $e) { $this->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Catalog rule indexing failed. See details in exception log.") ); } @@ -375,65 +369,6 @@ protected function cleanByIds($productIds) $this->cleanProductPriceIndex($productIds); } - /** - * Assign product to rule - * - * @param Rule $rule - * @param int $productEntityId - * @param array $websiteIds - * @return void - * @throws \Exception - * @deprecated - * @see ReindexRuleProduct::execute - */ - private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void - { - $ruleId = (int) $rule->getId(); - $ruleProductTable = $this->getTable('catalogrule_product'); - $this->connection->delete( - $ruleProductTable, - [ - 'rule_id = ?' => $ruleId, - 'product_id = ?' => $productEntityId, - ] - ); - - $customerGroupIds = $rule->getCustomerGroupIds(); - $fromTime = strtotime($rule->getFromDate()); - $toTime = strtotime($rule->getToDate()); - $toTime = $toTime ? $toTime + self::SECONDS_IN_DAY - 1 : 0; - $sortOrder = (int)$rule->getSortOrder(); - $actionOperator = $rule->getSimpleAction(); - $actionAmount = $rule->getDiscountAmount(); - $actionStop = $rule->getStopRulesProcessing(); - - $rows = []; - foreach ($websiteIds as $websiteId) { - foreach ($customerGroupIds as $customerGroupId) { - $rows[] = [ - 'rule_id' => $ruleId, - 'from_time' => $fromTime, - 'to_time' => $toTime, - 'website_id' => $websiteId, - 'customer_group_id' => $customerGroupId, - 'product_id' => $productEntityId, - 'action_operator' => $actionOperator, - 'action_amount' => $actionAmount, - 'action_stop' => $actionStop, - 'sort_order' => $sortOrder, - ]; - - if (count($rows) == $this->batchCount) { - $this->connection->insertMultiple($ruleProductTable, $rows); - $rows = []; - } - } - } - if ($rows) { - $this->connection->insertMultiple($ruleProductTable, $rows); - } - } - /** * Apply rule * @@ -457,30 +392,6 @@ protected function applyRule(Rule $rule, $product) return $this; } - /** - * Apply rules - * - * @param RuleCollection $ruleCollection - * @param Product $product - * @deprecated - * @see ReindexRuleProduct::execute - * @return void - */ - private function applyRules(RuleCollection $ruleCollection, Product $product): void - { - foreach ($ruleCollection as $rule) { - if (!$rule->validate($product)) { - continue; - } - - $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); - $this->assignProductToRule($rule, $product->getId(), $websiteIds); - } - - $this->cleanProductPriceIndex([$product->getId()]); - $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId()); - } - /** * Retrieve table name * @@ -580,7 +491,7 @@ protected function calcRuleProductPrice($ruleData, $productData = null) * @param int $websiteId * @param Product|null $product * @return \Zend_Db_Statement_Interface - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @deprecated 100.2.0 * @see RuleProductsSelectBuilder::build */ From 46a769fe55b4807a9e3031dd7720117b7bee4621 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Thu, 10 Sep 2020 13:28:20 +0300 Subject: [PATCH 005/137] magento/magento2#29549: Scheduled price rule time zone correction - integration test fix. --- .../Model/Indexer/Product/PriceTest.php | 9 +++------ .../_files/second_website_with_two_stores.php | 16 ++++++++++++++-- .../second_website_with_two_stores_rollback.php | 8 ++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php index 2b18b1569aaeb..4dc07309ee70b 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php @@ -21,6 +21,7 @@ class PriceTest extends \PHPUnit\Framework\TestCase * @var \Magento\Framework\ObjectManagerInterface */ private $objectManager; + /** * @var Rule */ @@ -95,15 +96,11 @@ public function testPriceForSecondStore():void { $websiteId = $this->websiteRepository->get('test')->getId(); $simpleProduct = $this->productRepository->get('simple'); - $simpleProduct->setPriceCalculation(true); $this->assertEquals('simple', $simpleProduct->getSku()); - $this->assertFalse( - $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()) - ); $this->indexerBuilder->reindexById($simpleProduct->getId()); $this->assertEquals( - $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()), - 25 + 25, + $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()) ); } diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php index 016acca1e8e04..47255f67ddbf0 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php @@ -10,11 +10,23 @@ $website->save(); } $websiteId = $website->getId(); + +$storeGroup = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Group::class); +if (!$storeGroup->load('fixture_second_store_group', 'code')->getId()) { + $storeGroup->setCode('fixture_second_store_group') + ->setName('Fixture Second Store Group') + ->setWebsite($website); + $storeGroup->save(); + + $website->setDefaultGroupId($storeGroup->getId()); + $website->save(); +} + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); if (!$store->load('fixture_second_store', 'code')->getId()) { $groupId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class - )->getWebsite()->getDefaultGroupId(); + )->getWebsite('test')->getDefaultGroupId(); $store->setCode( 'fixture_second_store' )->setWebsiteId( @@ -35,7 +47,7 @@ if (!$store->load('fixture_third_store', 'code')->getId()) { $groupId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class - )->getWebsite()->getDefaultGroupId(); + )->getWebsite('test')->getDefaultGroupId(); $store->setCode( 'fixture_third_store' )->setWebsiteId( diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php index eef8cf960944c..2b2a86ad55931 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php @@ -15,6 +15,14 @@ if ($websiteId) { $website->delete(); } + +$storeGroup = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Group::class); +/** @var $storeGroup \Magento\Store\Model\Group */ +$storeGroupId = $storeGroup->load('fixture_second_store_group', 'code')->getId(); +if ($storeGroupId) { + $storeGroup->delete(); +} + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); if ($store->load('fixture_second_store', 'code')->getId()) { $store->delete(); From e8507dbb9ee99d419ff46e8fff99ab27bec9af2b Mon Sep 17 00:00:00 2001 From: Yaroslav Bogutsky Date: Mon, 2 Nov 2020 21:37:43 +0300 Subject: [PATCH 006/137] MC-38592 Used store manager for retrieve current store by default in directory data helper --- app/code/Magento/Directory/Helper/Data.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 3133e3d4c1957..123a338a343d2 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -190,8 +190,8 @@ public function getRegionJson() \Magento\Framework\Profiler::start('TEST: ' . __METHOD__, ['group' => 'TEST', 'method' => __METHOD__]); if (!$this->_regionJson) { $scope = $this->getCurrentScope(); - $scopeKey = $scope['value'] ? '_' . implode('_', $scope) : null; - $cacheKey = 'DIRECTORY_REGIONS_JSON_STORE' . $scopeKey; + $scopeKey = $scope['value'] ? '_' . implode('_', $scope) : ''; + $cacheKey = 'DIRECTORY_REGIONS_JSON' . $scopeKey; $json = $this->_configCacheType->load($cacheKey); if (empty($json)) { $regions = $this->getRegionData(); @@ -406,14 +406,21 @@ public function getWeightUnit() * Get current scope from request * * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getCurrentScope(): array { - $scope = [ - 'type' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 'value' => null, - ]; + $scope = $this->_storeManager->getStore() + ? [ + 'type' => ScopeInterface::SCOPE_STORE, + 'value' => $this->_storeManager->getStore()->getId(), + ] + : [ + 'type' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 'value' => null, + ]; $request = $this->_getRequest(); + if ($request->getParam(ScopeInterface::SCOPE_WEBSITE)) { $scope = [ 'type' => ScopeInterface::SCOPE_WEBSITE, From 228cb684d982af6970c2248902a6a4c466eb01c7 Mon Sep 17 00:00:00 2001 From: Yaroslav Bogutsky Date: Tue, 3 Nov 2020 00:18:08 +0300 Subject: [PATCH 007/137] MC-38592 Extended unit tests of directory helper for testing get regions of different scopes --- .../Directory/Test/Unit/Helper/DataTest.php | 145 ++++++++++++++++-- 1 file changed, 128 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php index 2cb55a32b0772..adcf001847989 100644 --- a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php @@ -7,6 +7,7 @@ namespace Magento\Directory\Test\Unit\Helper; +use ArrayIterator; use Magento\Directory\Helper\Data; use Magento\Directory\Model\AllowedCountries; use Magento\Directory\Model\CurrencyFactory; @@ -52,6 +53,16 @@ class DataTest extends TestCase */ protected $_store; + /** + * @var RequestInterface|MockObject + */ + protected $_request; + + /** + * @var StoreManagerInterface|MockObject + */ + protected $_storeManager; + /** * @var ScopeConfigInterface|MockObject */ @@ -67,10 +78,10 @@ protected function setUp(): void $objectManager = new ObjectManager($this); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(false); - $requestMock = $this->getMockForAbstractClass(RequestInterface::class); + $this->_request = $this->getMockForAbstractClass(RequestInterface::class); $context = $this->createMock(Context::class); $context->method('getRequest') - ->willReturn($requestMock); + ->willReturn($this->_request); $context->expects($this->any()) ->method('getScopeConfig') ->willReturn($this->scopeConfigMock); @@ -94,8 +105,7 @@ protected function setUp(): void $this->jsonHelperMock = $this->createMock(JsonDataHelper::class); $this->_store = $this->createMock(Store::class); - $storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); - $storeManager->expects($this->any())->method('getStore')->willReturn($this->_store); + $this->_storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); $currencyFactory = $this->createMock(CurrencyFactory::class); @@ -105,14 +115,103 @@ protected function setUp(): void 'countryCollection' => $this->_countryCollection, 'regCollectionFactory' => $regCollectionFactory, 'jsonHelper' => $this->jsonHelperMock, - 'storeManager' => $storeManager, + 'storeManager' => $this->_storeManager, 'currencyFactory' => $currencyFactory, ]; $this->_object = $objectManager->getObject(Data::class, $arguments); } - public function testGetRegionJson() + /** + * @return array + */ + public function regionJsonProvider(): array + { + $countries = [ + 'Country1' => [ + 'r1' => ['code' => 'r1-code', 'name' => 'r1-name'], + 'r2' => ['code' => 'r2-code', 'name' => 'r2-name'] + ], + 'Country2' => [ + 'r3' => ['code' => 'r3-code', 'name' => 'r3-name'], + ], + 'Country3' => [], + ]; + + return [ + [ + null, + $countries, + ], + [ + null, + [ + 'Country1' => $countries['Country1'], + ], + [ScopeInterface::SCOPE_WEBSITE => 1], + ], + [ + 1, + [ + 'Country2' => $countries['Country2'], + ], + ], + [ + null, + [ + 'Country2' => $countries['Country2'], + ], + [ + ScopeInterface::SCOPE_WEBSITE => null, + ScopeInterface::SCOPE_STORE => 1, + ], + ], + [ + 2, + [ + 'Country3' => $countries['Country3'], + ], + ], + [ + null, + [ + 'Country3' => $countries['Country3'], + ], + [ScopeInterface::SCOPE_STORE => 2], + ], + ]; + } + + /** + * @param int|null $currentStoreId + * @param array $allowedCountries + * @param array $requestParams + * @dataProvider regionJsonProvider + */ + public function testGetRegionJson(?int $currentStoreId, array $allowedCountries, array $requestParams = []) { + if ($currentStoreId) { + $this->_store->method('getId')->willReturn($currentStoreId); + $this->_storeManager->expects($this->any())->method('getStore')->willReturn($this->_store); + } else { + $this->_storeManager->expects($this->any())->method('getStore')->willReturn(null); + } + + if ($requestParams) { + $map = []; + + foreach ($requestParams as $name => $value) { + $map[] = [$name, null, $value]; + } + + $this->_request + ->method('getParam') + ->willReturnMap($map); + } + + $expectedDataToEncode = array_merge([ + 'config' => ['show_all_regions' => false, 'regions_required' => []], + ], array_filter($allowedCountries)); + $this->scopeConfigMock->method('getValue') ->willReturnMap( [ @@ -120,7 +219,25 @@ public function testGetRegionJson() AllowedCountries::ALLOWED_COUNTRIES_PATH, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - 'Country1,Country2' + 'Country1,Country2,Country3' + ], + [ + AllowedCountries::ALLOWED_COUNTRIES_PATH, + ScopeInterface::SCOPE_WEBSITE, + 1, + 'Country1' + ], + [ + AllowedCountries::ALLOWED_COUNTRIES_PATH, + ScopeInterface::SCOPE_STORE, + 1, + 'Country2' + ], + [ + AllowedCountries::ALLOWED_COUNTRIES_PATH, + ScopeInterface::SCOPE_STORE, + 2, + 'Country3' ], [Data::XML_PATH_STATES_REQUIRED, ScopeInterface::SCOPE_STORE, null, ''] ] @@ -136,14 +253,16 @@ public function testGetRegionJson() ['country_id' => 'Country2', 'region_id' => 'r3', 'code' => 'r3-code', 'name' => 'r3-name'] ) ]; - $regionIterator = new \ArrayIterator($regions); + $regionIterator = new ArrayIterator(array_filter($regions, function(DataObject $region) use ($allowedCountries) { + return array_key_exists($region->getData('country_id'), $allowedCountries); + })); $this->_regionCollection->expects( $this->once() )->method( 'addCountryFilter' )->with( - ['Country1', 'Country2'] + array_keys($allowedCountries) )->willReturnSelf(); $this->_regionCollection->expects($this->once())->method('load'); $this->_regionCollection->expects( @@ -154,14 +273,6 @@ public function testGetRegionJson() $regionIterator ); - $expectedDataToEncode = [ - 'config' => ['show_all_regions' => false, 'regions_required' => []], - 'Country1' => [ - 'r1' => ['code' => 'r1-code', 'name' => 'r1-name'], - 'r2' => ['code' => 'r2-code', 'name' => 'r2-name'] - ], - 'Country2' => ['r3' => ['code' => 'r3-code', 'name' => 'r3-name']] - ]; $this->jsonHelperMock->expects( $this->once() )->method( From 4e48a7abfa1c8f4dc711414992a06dcd7bef58d8 Mon Sep 17 00:00:00 2001 From: Yaroslav Bogutsky Date: Sun, 8 Nov 2020 00:50:56 +0300 Subject: [PATCH 008/137] MC-38592 Fixed code styles of unit tests --- .../Directory/Test/Unit/Helper/DataTest.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php index adcf001847989..f9da2bdd6912a 100644 --- a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php @@ -242,20 +242,17 @@ public function testGetRegionJson(?int $currentStoreId, array $allowedCountries, [Data::XML_PATH_STATES_REQUIRED, ScopeInterface::SCOPE_STORE, null, ''] ] ); + $regions = [ - new DataObject( - ['country_id' => 'Country1', 'region_id' => 'r1', 'code' => 'r1-code', 'name' => 'r1-name'] - ), - new DataObject( - ['country_id' => 'Country1', 'region_id' => 'r2', 'code' => 'r2-code', 'name' => 'r2-name'] - ), - new DataObject( - ['country_id' => 'Country2', 'region_id' => 'r3', 'code' => 'r3-code', 'name' => 'r3-name'] - ) + new DataObject(['country_id' => 'Country1', 'region_id' => 'r1', 'code' => 'r1-code', 'name' => 'r1-name']), + new DataObject(['country_id' => 'Country1', 'region_id' => 'r2', 'code' => 'r2-code', 'name' => 'r2-name']), + new DataObject(['country_id' => 'Country2', 'region_id' => 'r3', 'code' => 'r3-code', 'name' => 'r3-name']), ]; - $regionIterator = new ArrayIterator(array_filter($regions, function(DataObject $region) use ($allowedCountries) { + $regions = array_filter($regions, function (DataObject $region) use ($allowedCountries) { return array_key_exists($region->getData('country_id'), $allowedCountries); - })); + }); + + $regionIterator = new ArrayIterator($regions); $this->_regionCollection->expects( $this->once() From d9a0f6f6ca0d547d758d01ab9fc6d6c19956120f Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 5 Jan 2021 18:17:57 +0200 Subject: [PATCH 009/137] magento/magento2#29549: Scheduled price rule time zone correction - test coverage. --- .../Unit/Model/Indexer/IndexBuilderTest.php | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php new file mode 100644 index 0000000000000..d4e64a684e4e1 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php @@ -0,0 +1,195 @@ +ruleCollectionFactoryMock = $this->createMock(CollectionFactory::class); + $this->priceCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class) + ->getMockForAbstractClass(); + $this->resourceMock = $this->createMock(ResourceConnection::class); + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->resourceMock->expects($this->once())->method('getConnection') + ->willReturn($this->connectionMock); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->getMockForAbstractClass(); + $this->eavConfigMock = $this->createMock(Config::class); + $this->dateFormatMock = $this->createMock(DateTime::class); + $this->dateTimeMock = $this->createMock(DateTime\DateTime::class); + $this->productFactoryMock = $this->createMock(ProductFactory::class); + $this->batchCountMock = 99; + $this->reindexRuleProductMock = $this->createMock(ReindexRuleProduct::class); + $this->reindexRuleProductPriceMock = $this->createMock(ReindexRuleProductPrice::class); + + $this->indexBuilder = $objectManager->getObject( + IndexBuilder::class, + [ + 'ruleCollectionFactory' => $this->ruleCollectionFactoryMock, + 'priceCurrency' => $this->priceCurrencyMock, + 'resource' => $this->resourceMock, + 'storeManager' => $this->storeManagerMock, + 'logger' => $this->loggerMock, + 'eavConfig' => $this->eavConfigMock, + 'dateFormat' => $this->dateFormatMock, + 'dateTime' => $this->dateTimeMock, + 'productFactory' => $this->productFactoryMock, + 'batchCount' => $this->batchCountMock, + 'reindexRuleProduct' => $this->reindexRuleProductMock, + 'reindexRuleProductPrice' => $this->reindexRuleProductPriceMock, + ] + ); + } + + /** + * Test for \Magento\CatalogRule\Model\Indexer\IndexBuilder::reindexByIds. + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testReindexByIds() + { + $id1 = 1; + $id2 = 1; + $ids = [$id1, $id2]; + $collectionMock = $this->createMock(Collection::class); + $collectionMock->expects($this->once())->method('addFieldToFilter')->with('is_active', 1) + ->willReturn($collectionMock); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once())->method('setProductsFilter')->with($ids); + $collectionMock->expects($this->once())->method('getItems')->willReturn([$ruleMock]); + $this->ruleCollectionFactoryMock->expects($this->once())->method('create') + ->willReturn($collectionMock); + $this->reindexRuleProductMock->expects($this->once())->method('execute') + ->with($ruleMock, $this->batchCountMock); + $this->reindexRuleProductPriceMock->expects($this->exactly(2))->method('execute') + ->withConsecutive([$this->batchCountMock, $id1], [$this->batchCountMock, $id2]); + + $this->indexBuilder->reindexByIds($ids); + } + + /** + * Test for \Magento\CatalogRule\Model\Indexer\IndexBuilder::reindexById. + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testReindexById() + { + $id = 1; + $collectionMock = $this->createMock(Collection::class); + $collectionMock->expects($this->once())->method('addFieldToFilter')->with('is_active', 1) + ->willReturn($collectionMock); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once())->method('setProductsFilter')->with([$id]); + $collectionMock->expects($this->once())->method('getItems')->willReturn([$ruleMock]); + $this->ruleCollectionFactoryMock->expects($this->once())->method('create') + ->willReturn($collectionMock); + $this->reindexRuleProductMock->expects($this->once())->method('execute') + ->with($ruleMock, $this->batchCountMock); + $this->reindexRuleProductPriceMock->expects($this->once())->method('execute') + ->with($this->batchCountMock, $id); + + $this->indexBuilder->reindexById($id); + } +} From 42d5ef9c38cb4c97652f4774ece446641d3f2ab2 Mon Sep 17 00:00:00 2001 From: Oleksandr Melnyk Date: Wed, 6 Jan 2021 18:05:00 +0200 Subject: [PATCH 010/137] magento/magento2#31332:Implement the schema changes for Configurable Options Selection - resolved fields according to the new schema --- .../Products/DataProvider/ProductSearch.php | 10 ++ .../Model/Formatter/Option.php | 62 ++++++++ .../Model/Formatter/OptionValue.php | 65 ++++++++ .../Model/Formatter/Variant.php | 49 ++++++ .../Model/Options/DataProvider/Variant.php | 10 +- .../Model/Options/Metadata.php | 141 ++++-------------- .../Model/Options/SelectionUidFormatter.php | 29 +++- .../Model/Resolver/OptionsSelection.php | 97 ++++++++++++ .../Model/Resolver/SelectionMediaGallery.php | 28 +++- .../ConfigurableProductGraphQl/composer.json | 1 + .../ConfigurableProductGraphQl/etc/module.xml | 1 + .../etc/schema.graphqls | 30 +++- 12 files changed, 390 insertions(+), 133 deletions(-) create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 13bd29e83d87f..4350875ccca7c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -18,6 +18,8 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\GraphQl\Model\Query\ContextInterface; /** @@ -87,6 +89,7 @@ public function __construct( * @param array $attributes * @param ContextInterface|null $context * @return SearchResultsInterface + * @throws GraphQlNoSuchEntityException */ public function getList( SearchCriteriaInterface $searchCriteria, @@ -107,6 +110,13 @@ public function getList( )->apply(); $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); + + try { + $collection->addMediaGalleryData(); + } catch (LocalizedException $e) { + throw new GraphQlNoSuchEntityException(__('Cannot load media galery')); + } + $collection->load(); $this->collectionPostProcessor->process($collection, $attributes); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php new file mode 100644 index 0000000000000..240dca5def4cf --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php @@ -0,0 +1,62 @@ +idEncoder = $idEncoder; + $this->valueFormatter = $valueFormatter; + } + + /** + * Format configurable product options according to the GraphQL schema + * + * @param Attribute $attribute + * @return array|null + */ + public function format(Attribute $attribute): ?array + { + $optionValues = []; + + foreach ($attribute->getOptions() as $option) { + $optionValues[] = $this->valueFormatter->format($option, $attribute); + } + + return [ + 'uid' => $this->idEncoder->encode($attribute->getProductSuperAttributeId()), + 'attribute_code' => $attribute->getProductAttribute()->getAttributeCode(), + 'label' => $attribute->getLabel(), + 'values' => $optionValues, + ]; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php new file mode 100644 index 0000000000000..3a991c3bca635 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php @@ -0,0 +1,65 @@ +selectionUidFormatter = $selectionUidFormatter; + $this->swatchDataProvider = $swatchDataProvider; + } + + /** + * Format configurable product option values according to the GraphQL schema + * + * @param array $optionValue + * @param Attribute $attribute + * @return array + */ + public function format(array $optionValue, Attribute $attribute): array + { + $valueIndex = (int)$optionValue['value_index']; + $attributeId = (int)$attribute->getAttributeId(); + + return [ + 'uid' => $this->selectionUidFormatter->encode( + $attributeId, + $valueIndex + ), + 'is_available' => true, + 'is_default' => (bool)$attribute->getIsUseDefault(), + 'label' => $optionValue['label'], + 'swatch' => $this->swatchDataProvider->getData($optionValue['value_index']) + ]; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php new file mode 100644 index 0000000000000..1d73ad6a19336 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php @@ -0,0 +1,49 @@ + $selectedValue) { + if (!isset($options[$attributeId][$selectedValue])) { + throw new GraphQlInputException(__('configurableOptionValueUids values are incorrect')); + } + + $productIds = array_intersect($productIds, $options[$attributeId][$selectedValue]); + } + + if (count($productIds) === 1) { + $variantProduct = $variants[array_pop($productIds)]; + $variant = $variantProduct->getData(); + $variant['url_path'] = $variantProduct->getProductUrl(); + $variant['model'] = $variantProduct; + } + + return $variant; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php index 80fbdc76bacb3..71671353facf5 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php @@ -10,6 +10,8 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; /** * Retrieve child products @@ -42,10 +44,10 @@ public function __construct( * Load available child products by parent * * @param ProductInterface $product - * @return ProductInterface[] - * @throws \Magento\Framework\Exception\LocalizedException + * @return DataObject[] + * @throws LocalizedException */ - public function getSalableVariantsByParent(ProductInterface $product) + public function getSalableVariantsByParent(ProductInterface $product): array { $collection = $this->configurableType->getUsedProductCollection($product); $collection @@ -62,6 +64,6 @@ public function getSalableVariantsByParent(ProductInterface $product) } $collection->clear(); - return $collection->getItems(); + return $collection->getItems() ?? []; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php index 9fa6e4f23fa56..92bfe61ee9b2d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php @@ -3,15 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\ConfigurableProductGraphQl\Model\Options; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\ConfigurableProduct\Helper\Data; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; +use Magento\ConfigurableProductGraphQl\Model\Formatter\Option; use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\ConfigurableProductGraphQl\Model\Formatter\Variant as VariantFormatter; /** * Retrieve metadata for configurable option selection. @@ -24,150 +26,63 @@ class Metadata private $configurableProductHelper; /** - * @var SelectionUidFormatter - */ - private $selectionUidFormatter; - - /** - * @var ProductRepositoryInterface + * @var Option */ - private $productRepository; - - /** - * @var Variant - */ - private $variant; + private $configurableOptionsFormatter; /** * @param Data $configurableProductHelper - * @param SelectionUidFormatter $selectionUidFormatter - * @param ProductRepositoryInterface $productRepository - * @param Variant $variant + * @param Option $configurableOptionsFormatter */ public function __construct( Data $configurableProductHelper, - SelectionUidFormatter $selectionUidFormatter, - ProductRepositoryInterface $productRepository, - Variant $variant + Option $configurableOptionsFormatter + ) { $this->configurableProductHelper = $configurableProductHelper; - $this->selectionUidFormatter = $selectionUidFormatter; - $this->productRepository = $productRepository; - $this->variant = $variant; + $this->configurableOptionsFormatter = $configurableOptionsFormatter; } /** - * Load available selections from configurable options. + * Load available selections from configurable options and variant. * * @param ProductInterface $product - * @param array $selectedOptionsUid + * @param array $options + * @param array $selectedOptions * @return array - * @throws NoSuchEntityException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function getAvailableSelections( - ProductInterface $product, - array $selectedOptionsUid - ): array { - $options = $this->configurableProductHelper->getOptions($product, $this->getAllowProducts($product)); - $selectedOptions = $this->selectionUidFormatter->extract($selectedOptionsUid); - $attributeCodes = $this->getAttributeCodes($product); - $availableSelections = $availableProducts = $variantData = []; - - if (isset($options['index']) && $options['index']) { - foreach ($options['index'] as $productId => $productOptions) { - if (!empty($selectedOptions) && !$this->hasProductRequiredOptions($selectedOptions, $productOptions)) { - continue; - } - - $availableProducts[] = $productId; - foreach ($productOptions as $attributeId => $optionIndex) { - $uid = $this->selectionUidFormatter->encode($attributeId, (int)$optionIndex); + public function getAvailableSelections(ProductInterface $product, array $options, array $selectedOptions): array + { + $attributes = $this->getAttributes($product); - if (isset($availableSelections[$attributeId]['option_value_uids']) - && in_array($uid, $availableSelections[$attributeId]['option_value_uids']) - ) { - continue; - } - $availableSelections[$attributeId]['option_value_uids'][] = $uid; - $availableSelections[$attributeId]['attribute_code'] = $attributeCodes[$attributeId]; - } + $availableSelections = []; - if ($this->hasSelectionProduct($selectedOptions, $productOptions)) { - $variantProduct = $this->productRepository->getById($productId); - $variantData = $variantProduct->getData(); - $variantData['model'] = $variantProduct; - } + foreach ($options as $attributeId => $option) { + if ($attributeId === 'index' || isset($selectedOptions[$attributeId])) { + continue; } - } - return [ - 'options_available_for_selection' => $availableSelections, - 'variant' => $variantData, - 'availableSelectionProducts' => array_unique($availableProducts), - 'product' => $product - ]; - } - - /** - * Get allowed products. - * - * @param ProductInterface $product - * @return ProductInterface[] - */ - public function getAllowProducts(ProductInterface $product): array - { - return $this->variant->getSalableVariantsByParent($product) ?? []; - } - - /** - * Check if a product has the selected options. - * - * @param array $requiredOptions - * @param array $productOptions - * @return bool - */ - private function hasProductRequiredOptions($requiredOptions, $productOptions): bool - { - $result = true; - foreach ($requiredOptions as $attributeId => $optionIndex) { - if (!isset($productOptions[$attributeId]) || !$productOptions[$attributeId] - || $optionIndex != $productOptions[$attributeId] - ) { - $result = false; - break; - } + $availableSelections[] = $this->configurableOptionsFormatter->format($attributes[$attributeId]); } - return $result; - } + return $availableSelections; - /** - * Check if selected options match a product. - * - * @param array $requiredOptions - * @param array $productOptions - * @return bool - */ - private function hasSelectionProduct($requiredOptions, $productOptions): bool - { - return $this->hasProductRequiredOptions($productOptions, $requiredOptions); } /** - * Retrieve attribute codes + * Retrieve configurable attributes for the product * * @param ProductInterface $product - * @return string[] + * @return Attribute[] */ - private function getAttributeCodes(ProductInterface $product): array + private function getAttributes(ProductInterface $product): array { $allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product); - $attributeCodes = []; + $attributes = []; foreach ($allowedAttributes as $attribute) { - $attributeCodes[$attribute->getAttributeId()] = $attribute->getProductAttribute()->getAttributeCode(); + $attributes[$attribute->getAttributeId()] = $attribute; } - return $attributeCodes; + return $attributes; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php index 1d13ad75489a1..7bcf08a9b9509 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProductGraphQl\Model\Options; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Uid; + /** * Handle option selection uid. */ @@ -20,6 +24,19 @@ class SelectionUidFormatter */ private const UID_SEPARATOR = '/'; + /** + * @var Uid + */ + private $idEncoder; + + /** + * @param Uid $idEncoder + */ + public function __construct(Uid $idEncoder) + { + $this->idEncoder = $idEncoder; + } + /** * Create uid and encode. * @@ -29,8 +46,7 @@ class SelectionUidFormatter */ public function encode(int $attributeId, int $indexId): string { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return base64_encode(implode(self::UID_SEPARATOR, [ + return $this->idEncoder->encode(implode(self::UID_SEPARATOR, [ self::UID_PREFIX, $attributeId, $indexId @@ -40,17 +56,16 @@ public function encode(int $attributeId, int $indexId): string /** * Retrieve attribute and option index from uid. Array key is the id of attribute and value is the index of option * - * @param string $selectionUids + * @param array $selectionUids * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws GraphQlInputException */ public function extract(array $selectionUids): array { $attributeOption = []; foreach ($selectionUids as $uid) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $optionData = explode(self::UID_SEPARATOR, base64_decode($uid)); - if (count($optionData) == 3) { + $optionData = explode(self::UID_SEPARATOR, $this->idEncoder->decode($uid)); + if (count($optionData) === 3) { $attributeOption[(int)$optionData[1]] = (int)$optionData[2]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php new file mode 100644 index 0000000000000..6d1e4cbf5bb00 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php @@ -0,0 +1,97 @@ +configurableSelectionMetadata = $configurableSelectionMetadata; + $this->selectionUidFormatter = $selectionUidFormatter; + $this->variant = $variant; + $this->variantFormatter = $variantFormatter; + $this->configurableProductHelper = $configurableProductHelper; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $product = $value['model']; + + $selectionUids = $args['configurableOptionValueUids'] ?? []; + $selectedOptions = $this->selectionUidFormatter->extract($selectionUids); + + $variants = $this->variant->getSalableVariantsByParent($product); + $options = $this->configurableProductHelper->getOptions($product, $variants); + + return [ + 'configurable_options' => $this->configurableSelectionMetadata->getAvailableSelections( + $product, + $options, + $selectedOptions + ), + 'variant' => $this->variantFormatter->format($options, $selectedOptions, $variants), + 'model' => $product + ]; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php index 7b3ddc4ac1417..b6e7574fc20d8 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php @@ -30,8 +30,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value foreach ($usedProducts as $usedProduct) { if (in_array($usedProduct->getId(), $availableSelectionProducts)) { foreach ($usedProduct->getMediaGalleryEntries() ?? [] as $key => $entry) { - $index = $usedProduct->getId() . '_' . $key; - $mediaGalleryEntries[$index] = $entry->getData(); + $entryData = $entry->getData(); + $initialIndex = $usedProduct->getId() . '_' . $key; + $index = $this->prepareIndex($entryData, $initialIndex); + $mediaGalleryEntries[$index] = $entryData; $mediaGalleryEntries[$index]['model'] = $usedProduct; if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { $mediaGalleryEntries[$index]['video_content'] @@ -42,4 +44,26 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } return $mediaGalleryEntries; } + + /** + * Formulate an index to have unique set of media entries + * + * @param array $entryData + * @param string $initialIndex + * @return string + */ + private function prepareIndex(array $entryData, string $initialIndex) : string + { + $index = $initialIndex; + if (isset($entryData['media_type'])) { + $index = $entryData['media_type']; + } + if (isset($entryData['file'])) { + $index = $index.'_'.$entryData['file']; + } + if (isset($entryData['position'])) { + $index = $index.'_'.$entryData['position']; + } + return $index; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index a6e1d1c822435..72ecdbc3a375f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -11,6 +11,7 @@ "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", "magento/module-catalog-inventory": "*", + "magento/module-swatches-graph-ql": "*", "magento/framework": "*" }, "license": [ diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml index 3aa1658c9388d..e6345ac188631 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml @@ -14,6 +14,7 @@ + diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index fc177557906ee..dcfcc426f239c 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -7,7 +7,29 @@ type Mutation { type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") - configurable_options_selection_metadata(selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata") + configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelection") +} + +type ConfigurableProductOptionsSelection @doc(description: "Metadata corresponding to the configurable options selection.") +{ + configurable_options: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.") + media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery") + variant: SimpleProduct @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") +} + +type ConfigurableProductOption { + uid: ID! + attribute_code: String! + label: String! + values: [ConfigurableProductOptionValue!] +} + +type ConfigurableProductOptionValue { + uid: ID! + is_available: Boolean! + is_use_default: Boolean! + label: String! + swatch: SwatchDataInterface } type ConfigurableVariant @doc(description: "An array containing all the simple product variants of a configurable product") { @@ -80,12 +102,6 @@ type ConfigurableWishlistItem implements WishlistItemInterface @doc(description: configurable_options: [SelectedConfigurableOption!] @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Wishlist\\ConfigurableOptions") @doc (description: "An array of selected configurable options") } -type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { - options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") - media_gallery: [MediaGalleryInterface!] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\SelectionMediaGallery") @doc(description: "Product images and videos corresponding to the specified configurable options selection.") - variant: SimpleProduct @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Variant") @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") -} - type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") From 6bf3dfcf5d4215f9408f8a2a68c2a64b8f67cb9d Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Fri, 8 Jan 2021 17:19:08 +0200 Subject: [PATCH 011/137] magento/magento2#29549: Scheduled price rule time zone correction - refactoring. --- .../Model/Indexer/IndexBuilder.php | 129 +++++++++++++++++- 1 file changed, 124 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 9f4881fdf8a0b..38b48e05c55c2 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule; @@ -17,9 +19,9 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Pricing\PriceCurrencyInterface; -use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; -use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; @@ -143,11 +145,26 @@ class IndexBuilder */ private $pricesPersistor; + /** + * @var TimezoneInterface|mixed + */ + private $localeDate; + + /** + * @var ActiveTableSwitcher|mixed + */ + private $activeTableSwitcher; + /** * @var TableSwapper */ private $tableSwapper; + /** + * @var ProductLoader|mixed + */ + private $productLoader; + /** * @param RuleCollectionFactory $ruleCollectionFactory * @param PriceCurrencyInterface $priceCurrency @@ -168,6 +185,7 @@ class IndexBuilder * @param ActiveTableSwitcher|null $activeTableSwitcher * @param ProductLoader|null $productLoader * @param TableSwapper|null $tableSwapper + * @param TimezoneInterface|null $localeDate * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -190,7 +208,8 @@ public function __construct( RuleProductPricesPersistor $pricesPersistor = null, ActiveTableSwitcher $activeTableSwitcher = null, ProductLoader $productLoader = null, - TableSwapper $tableSwapper = null + TableSwapper $tableSwapper = null, + TimezoneInterface $localeDate = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -222,8 +241,16 @@ public function __construct( $this->pricesPersistor = $pricesPersistor ?? ObjectManager::getInstance()->get( RuleProductPricesPersistor::class ); + $this->activeTableSwitcher = $activeTableSwitcher ?? ObjectManager::getInstance()->get( + ActiveTableSwitcher::class + ); + $this->productLoader = $productLoader ?? ObjectManager::getInstance()->get( + ProductLoader::class + ); $this->tableSwapper = $tableSwapper ?? ObjectManager::getInstance()->get(TableSwapper::class); + $this->localeDate = $localeDate ?? + ObjectManager::getInstance()->get(TimezoneInterface::class); } /** @@ -237,11 +264,19 @@ public function __construct( public function reindexById($id) { try { - $this->doReindexByIds([$id]); + $this->cleanProductIndex([$id]); + + $products = $this->productLoader->getProducts([$id]); + $activeRules = $this->getActiveRules(); + foreach ($products as $product) { + $this->applyRules($activeRules, $product); + } + + $this->reindexRuleGroupWebsite->execute(); } catch (\Exception $e) { $this->critical($e); throw new LocalizedException( - __("Catalog rule indexing failed. See details in exception log.") + __('Catalog rule indexing failed. See details in exception log.') ); } } @@ -369,6 +404,68 @@ protected function cleanByIds($productIds) $this->cleanProductPriceIndex($productIds); } + /** + * Assign product to rule + * + * @param Rule $rule + * @param int $productEntityId + * @param array $websiteIds + * @return void + */ + private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void + { + $ruleId = (int) $rule->getId(); + $ruleProductTable = $this->getTable('catalogrule_product'); + $this->connection->delete( + $ruleProductTable, + [ + 'rule_id = ?' => $ruleId, + 'product_id = ?' => $productEntityId, + ] + ); + + $customerGroupIds = $rule->getCustomerGroupIds(); + $sortOrder = (int)$rule->getSortOrder(); + $actionOperator = $rule->getSimpleAction(); + $actionAmount = $rule->getDiscountAmount(); + $actionStop = $rule->getStopRulesProcessing(); + + $rows = []; + foreach ($websiteIds as $websiteId) { + $scopeTz = new \DateTimeZone( + $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) + ); + $fromTime = $rule->getFromDate() + ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp() + : 0; + $toTime = $rule->getToDate() + ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 + : 0; + foreach ($customerGroupIds as $customerGroupId) { + $rows[] = [ + 'rule_id' => $ruleId, + 'from_time' => $fromTime, + 'to_time' => $toTime, + 'website_id' => $websiteId, + 'customer_group_id' => $customerGroupId, + 'product_id' => $productEntityId, + 'action_operator' => $actionOperator, + 'action_amount' => $actionAmount, + 'action_stop' => $actionStop, + 'sort_order' => $sortOrder, + ]; + + if (count($rows) == $this->batchCount) { + $this->connection->insertMultiple($ruleProductTable, $rows); + $rows = []; + } + } + } + if ($rows) { + $this->connection->insertMultiple($ruleProductTable, $rows); + } + } + /** * Apply rule * @@ -392,6 +489,28 @@ protected function applyRule(Rule $rule, $product) return $this; } + /** + * Apply rules + * + * @param RuleCollection $ruleCollection + * @param Product $product + * @return void + */ + private function applyRules(RuleCollection $ruleCollection, Product $product): void + { + foreach ($ruleCollection as $rule) { + if (!$rule->validate($product)) { + continue; + } + + $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); + $this->assignProductToRule($rule, $product->getId(), $websiteIds); + } + + $this->cleanProductPriceIndex([$product->getId()]); + $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId()); + } + /** * Retrieve table name * From 55f90b4c5c094914a3ed2b1efddbc07c75ea9089 Mon Sep 17 00:00:00 2001 From: Oleksandr Melnyk Date: Tue, 12 Jan 2021 17:09:01 +0200 Subject: [PATCH 012/137] magento/magento2#31332:Schema changes implementation for configurable options selection - added test coverage --- .../Products/DataProvider/ProductSearch.php | 2 +- .../Model/Formatter/Option.php | 5 +- .../Model/Formatter/OptionValue.php | 35 +- .../Model/Options/Metadata.php | 10 +- .../Resolver/OptionsSelectionMetadata.php | 49 --- .../etc/schema.graphqls | 5 - ...nfigurableOptionsSelectionMetadataTest.php | 410 ------------------ .../ConfigurableOptionsSelectionTest.php | 348 +++++++++++++++ 8 files changed, 387 insertions(+), 477 deletions(-) delete mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php delete mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 4350875ccca7c..bcf0aa15b9e63 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -114,7 +114,7 @@ public function getList( try { $collection->addMediaGalleryData(); } catch (LocalizedException $e) { - throw new GraphQlNoSuchEntityException(__('Cannot load media galery')); + throw new GraphQlNoSuchEntityException(__('Cannot load media gallery')); } $collection->load(); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php index 240dca5def4cf..68968b6f3819a 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php @@ -42,14 +42,15 @@ public function __construct( * Format configurable product options according to the GraphQL schema * * @param Attribute $attribute + * @param array $optionIds * @return array|null */ - public function format(Attribute $attribute): ?array + public function format(Attribute $attribute, array $optionIds): ?array { $optionValues = []; foreach ($attribute->getOptions() as $option) { - $optionValues[] = $this->valueFormatter->format($option, $attribute); + $optionValues[] = $this->valueFormatter->format($option, $attribute, $optionIds); } return [ diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php index 3a991c3bca635..f171514a37897 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php @@ -8,6 +8,7 @@ namespace Magento\ConfigurableProductGraphQl\Model\Formatter; +use Magento\CatalogInventory\Model\StockRegistry; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; use Magento\SwatchesGraphQl\Model\Resolver\Product\Options\DataProvider\SwatchDataProvider; @@ -27,16 +28,24 @@ class OptionValue */ private $swatchDataProvider; + /** + * @var StockRegistry + */ + private $stockRegistry; + /** * @param SelectionUidFormatter $selectionUidFormatter * @param SwatchDataProvider $swatchDataProvider + * @param StockRegistry $stockRegistry */ public function __construct( SelectionUidFormatter $selectionUidFormatter, - SwatchDataProvider $swatchDataProvider + SwatchDataProvider $swatchDataProvider, + StockRegistry $stockRegistry ) { $this->selectionUidFormatter = $selectionUidFormatter; $this->swatchDataProvider = $swatchDataProvider; + $this->stockRegistry = $stockRegistry; } /** @@ -44,9 +53,10 @@ public function __construct( * * @param array $optionValue * @param Attribute $attribute + * @param array $optionIds * @return array */ - public function format(array $optionValue, Attribute $attribute): array + public function format(array $optionValue, Attribute $attribute, array $optionIds): array { $valueIndex = (int)$optionValue['value_index']; $attributeId = (int)$attribute->getAttributeId(); @@ -56,10 +66,27 @@ public function format(array $optionValue, Attribute $attribute): array $attributeId, $valueIndex ), - 'is_available' => true, - 'is_default' => (bool)$attribute->getIsUseDefault(), + 'is_available' => $this->getIsAvailable($optionIds[$valueIndex] ?? []), + 'is_use_default' => (bool)$attribute->getIsUseDefault(), 'label' => $optionValue['label'], 'swatch' => $this->swatchDataProvider->getData($optionValue['value_index']) ]; } + + /** + * Get is variants available + * + * @param array $variantIds + * @return bool + */ + private function getIsAvailable(array $variantIds): bool + { + foreach ($variantIds as $variantId) { + if ($this->stockRegistry->getProductStockStatus($variantId)) { + return true; + } + } + + return false; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php index 92bfe61ee9b2d..5e0c5535c564c 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php @@ -12,8 +12,6 @@ use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProductGraphQl\Model\Formatter\Option; -use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant; -use Magento\ConfigurableProductGraphQl\Model\Formatter\Variant as VariantFormatter; /** * Retrieve metadata for configurable option selection. @@ -37,7 +35,6 @@ class Metadata public function __construct( Data $configurableProductHelper, Option $configurableOptionsFormatter - ) { $this->configurableProductHelper = $configurableProductHelper; $this->configurableOptionsFormatter = $configurableOptionsFormatter; @@ -54,7 +51,6 @@ public function __construct( public function getAvailableSelections(ProductInterface $product, array $options, array $selectedOptions): array { $attributes = $this->getAttributes($product); - $availableSelections = []; foreach ($options as $attributeId => $option) { @@ -62,11 +58,13 @@ public function getAvailableSelections(ProductInterface $product, array $options continue; } - $availableSelections[] = $this->configurableOptionsFormatter->format($attributes[$attributeId]); + $availableSelections[] = $this->configurableOptionsFormatter->format( + $attributes[$attributeId], + $options[$attributeId] ?? [] + ); } return $availableSelections; - } /** diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php deleted file mode 100644 index f7d5a96ad2aba..0000000000000 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php +++ /dev/null @@ -1,49 +0,0 @@ -configurableSelectionMetadata = $configurableSelectionMetadata; - } - - /** - * @inheritDoc - */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - if (!isset($value['model'])) { - throw new LocalizedException(__('"model" value should be specified')); - } - - $selectedOptions = $args['selectedConfigurableOptionValues'] ?? []; - /** @var ProductInterface $product */ - $product = $value['model']; - - return $this->configurableSelectionMetadata->getAvailableSelections($product, $selectedOptions); - } -} diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index dcfcc426f239c..d9e65927a7d52 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -102,11 +102,6 @@ type ConfigurableWishlistItem implements WishlistItemInterface @doc(description: configurable_options: [SelectedConfigurableOption!] @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Wishlist\\ConfigurableOptions") @doc (description: "An array of selected configurable options") } -type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { - option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") - attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") -} - type StoreConfig @doc(description: "The type contains information about a store config") { configurable_thumbnail_source : String @doc(description: "The configuration setting determines which thumbnail should be used in the cart for configurable products.") } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php deleted file mode 100644 index f0e4df50794a3..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php +++ /dev/null @@ -1,410 +0,0 @@ -attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); - $this->selectionUidFormatter = Bootstrap::getObjectManager()->create(SelectionUidFormatter::class); - } - - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php - */ - public function testWithoutSelectedOption() - { - $query = <<graphQlQuery($query); - $this->assertEquals(1, count($response['products']['items'])); - $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'])); - $this->assertEquals(4, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][0]['option_value_uids'])); - $this->assertEquals(4, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][1]['option_value_uids'])); - } - - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php - */ - public function testSelectedFirstAttributeFirstOption() - { - $attribute = $this->getFirstConfigurableAttribute(); - $options = $attribute->getOptions(); - $firstOptionUid = $this->selectionUidFormatter->encode( - (int)$attribute->getAttributeId(), - (int)$options[1]->getValue() - ); - $query = <<graphQlQuery($query); - $this->assertEquals(1, count($response['products']['items'])); - $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'])); - $this->assertEquals(1, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][0]['option_value_uids'])); - $this->assertEquals($firstOptionUid, $response['products']['items'][0] - ['configurable_options_selection_metadata']['options_available_for_selection'][0]['option_value_uids'][0]); - $this->assertEquals(4, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][1]['option_value_uids'])); - - $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); - $this->assertAvailableOptionUids( - $this->getSecondConfigurableAttribute()->getAttributeId(), - $secondAttributeOptions, - $response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][1]['option_value_uids'] - ); - } - - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php - */ - public function testSelectedFirstAttributeLastOption() - { - $attribute = $this->getFirstConfigurableAttribute(); - $options = $attribute->getOptions(); - $lastOptionUid = $this->selectionUidFormatter->encode( - (int)$attribute->getAttributeId(), - (int)$options[4]->getValue() - ); - $query = <<graphQlQuery($query); - $this->assertEquals(1, count($response['products']['items'])); - $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'])); - $this->assertEquals(1, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][0]['option_value_uids'])); - $this->assertEquals($lastOptionUid, $response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][0]['option_value_uids'][0]); - $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][1]['option_value_uids'])); - $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); - unset($secondAttributeOptions[0]); - unset($secondAttributeOptions[1]); - unset($secondAttributeOptions[2]); - $this->assertAvailableOptionUids( - $this->getSecondConfigurableAttribute()->getAttributeId(), - $secondAttributeOptions, - $response['products']['items'][0]['configurable_options_selection_metadata'] - ['options_available_for_selection'][1]['option_value_uids'] - ); - } - - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php - */ - public function testSelectedVariant() - { - $firstAttribute = $this->getFirstConfigurableAttribute(); - $firstOptions = $firstAttribute->getOptions(); - $firstAttributeFirstOptionUid = $this->selectionUidFormatter->encode( - (int)$firstAttribute->getAttributeId(), - (int)$firstOptions[1]->getValue() - ); - $secodnAttribute = $this->getSecondConfigurableAttribute(); - $secondOptions = $secodnAttribute->getOptions(); - $secondAttributeFirstOptionUid = $this->selectionUidFormatter->encode( - (int)$secodnAttribute->getAttributeId(), - (int)$secondOptions[1]->getValue() - ); - $query = <<graphQlQuery($query); - $this->assertEquals(1, count($response['products']['items'])); - $this->assertNotNull($response['products']['items'][0]['configurable_options_selection_metadata'] - ['variant']); - $this->assertEquals( - 'simple_' . $firstOptions[1]->getValue() . '_' . $secondOptions[1]->getValue(), - $response['products']['items'][0]['configurable_options_selection_metadata'] - ['variant']['sku'] - ); - } - - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php - */ - public function testMediaGalleryForAll() - { - $query = <<graphQlQuery($query); - $this->assertEquals(1, count($response['products']['items'])); - $this->assertEquals(14, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['media_gallery'])); - } - - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php - */ - public function testMediaGalleryWithSelection() - { - $attribute = $this->getFirstConfigurableAttribute(); - $options = $attribute->getOptions(); - $lastOptionUid = $this->selectionUidFormatter->encode( - (int)$attribute->getAttributeId(), - (int)$options[4]->getValue() - ); - $query = <<graphQlQuery($query); - $this->assertEquals(1, count($response['products']['items'])); - $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] - ['media_gallery'])); - } - - /** - * Assert option uid. - * - * @param $attributeId - * @param $expectedOptions - * @param $selectedOptions - */ - private function assertAvailableOptionUids($attributeId, $expectedOptions, $selectedOptions) - { - unset($expectedOptions[0]); - foreach ($expectedOptions as $option) { - $this->assertContains( - $this->selectionUidFormatter->encode((int)$attributeId, (int)$option->getValue()), - $selectedOptions - ); - } - } - - /** - * Get first configurable attribute. - * - * @return AttributeInterface - * @throws NoSuchEntityException - */ - private function getFirstConfigurableAttribute() - { - if (!$this->firstConfigurableAttribute) { - $attributeCode = 'test_configurable_first'; - $this->firstConfigurableAttribute = $this->attributeRepository->get('catalog_product', $attributeCode); - } - - return $this->firstConfigurableAttribute; - } - - /** - * Get second configurable attribute. - * - * @return AttributeInterface - * @throws NoSuchEntityException - */ - private function getSecondConfigurableAttribute() - { - if (!$this->secondConfigurableAttribute) { - $attributeCode = 'test_configurable_second'; - $this->secondConfigurableAttribute = $this->attributeRepository->get('catalog_product', $attributeCode); - } - - return $this->secondConfigurableAttribute; - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php new file mode 100644 index 0000000000000..dc5817b7a15ff --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php @@ -0,0 +1,348 @@ +attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); + $this->selectionUidFormatter = Bootstrap::getObjectManager()->create(SelectionUidFormatter::class); + $this->indexerFactory = Bootstrap::getObjectManager()->create(IndexerFactory::class); + } + + /** + * Test the first option of the first attribute selected + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedFirstAttributeFirstOption(): void + { + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $sku = 'configurable_12345'; + $firstOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + (int)$options[1]->getValue() + ); + + $this->reindexAll(); + $response = $this->graphQlQuery($this->getQuery($sku, [$firstOptionUid])); + + self::assertNotEmpty($response['products']['items']); + $product = current($response['products']['items']); + self::assertEquals('ConfigurableProduct', $product['__typename']); + self::assertEquals($sku, $product['sku']); + self::assertNotEmpty($product['configurable_product_options_selection']['configurable_options']); + self::assertNull($product['configurable_product_options_selection']['variant']); + self::assertCount(1, $product['configurable_product_options_selection']['configurable_options']); + self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][0]['values']); + + $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getSecondConfigurableAttribute()->getAttributeId(), + $secondAttributeOptions, + $this->getOptionsUids( + $product['configurable_product_options_selection']['configurable_options'][0]['values'] + ) + ); + } + + /** + * Test selected variant + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedVariant(): void + { + $firstAttribute = $this->getFirstConfigurableAttribute(); + $firstOptions = $firstAttribute->getOptions(); + $firstAttributeFirstOptionUid = $this->selectionUidFormatter->encode( + (int)$firstAttribute->getAttributeId(), + (int)$firstOptions[1]->getValue() + ); + $secondAttribute = $this->getSecondConfigurableAttribute(); + $secondOptions = $secondAttribute->getOptions(); + $secondAttributeFirstOptionUid = $this->selectionUidFormatter->encode( + (int)$secondAttribute->getAttributeId(), + (int)$secondOptions[1]->getValue() + ); + + $sku = 'configurable_12345'; + + $this->reindexAll(); + $response = $this->graphQlQuery( + $this->getQuery($sku, [$firstAttributeFirstOptionUid, $secondAttributeFirstOptionUid]) + ); + + self::assertNotEmpty($response['products']['items']); + $product = current($response['products']['items']); + self::assertEquals('ConfigurableProduct', $product['__typename']); + self::assertEquals($sku, $product['sku']); + self::assertEmpty($product['configurable_product_options_selection']['configurable_options']); + self::assertNotNull($product['configurable_product_options_selection']['variant']); + } + + /** + * Test without selected options + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testWithoutSelectedOption(): void + { + $sku = 'configurable_12345'; + $this->reindexAll(); + $response = $this->graphQlQuery($this->getQuery($sku)); + + self::assertNotEmpty($response['products']['items']); + $product = current($response['products']['items']); + self::assertEquals('ConfigurableProduct', $product['__typename']); + self::assertEquals($sku, $product['sku']); + + self::assertNotEmpty($product['configurable_product_options_selection']['configurable_options']); + self::assertNull($product['configurable_product_options_selection']['variant']); + self::assertCount(2, $product['configurable_product_options_selection']['configurable_options']); + self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][0]['values']); + self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][1]['values']); + + $firstAttributeOptions = $this->getFirstConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getFirstConfigurableAttribute()->getAttributeId(), + $firstAttributeOptions, + $this->getOptionsUids( + $product['configurable_product_options_selection']['configurable_options'][0]['values'] + ) + ); + + $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getSecondConfigurableAttribute()->getAttributeId(), + $secondAttributeOptions, + $this->getOptionsUids( + $product['configurable_product_options_selection']['configurable_options'][1]['values'] + ) + ); + } + + /** + * Test with wrong selected options + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testWithWrongSelectedOptions(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('configurableOptionValueUids values are incorrect'); + + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $sku = 'configurable_12345'; + $firstOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + $options[1]->getValue() + 100 + ); + + $this->reindexAll(); + $this->graphQlQuery($this->getQuery($sku, [$firstOptionUid])); + } + + /** + * Get GraphQL query to test configurable product options selection + * + * @param string $productSku + * @param array $optionValueUids + * @param int $pageSize + * @param int $currentPage + * @return string + */ + private function getQuery( + string $productSku, + array $optionValueUids = [], + int $pageSize = 20, + int $currentPage = 1 + ): string { + if (empty($optionValueUids)) { + $configurableOptionValueUids = ''; + } else { + $configurableOptionValueUids = '(configurableOptionValueUids: ['; + foreach ($optionValueUids as $configurableOptionValueUid) { + $configurableOptionValueUids .= '"' . $configurableOptionValueUid . '",'; + } + $configurableOptionValueUids .= '])'; + } + + return <<firstConfigurableAttribute) { + $this->firstConfigurableAttribute = $this->attributeRepository->get( + 'catalog_product', + 'test_configurable_first' + ); + } + + return $this->firstConfigurableAttribute; + } + + /** + * Get second configurable attribute. + * + * @return AttributeInterface + * @throws NoSuchEntityException + */ + private function getSecondConfigurableAttribute(): AttributeInterface + { + if (!$this->secondConfigurableAttribute) { + $this->secondConfigurableAttribute = $this->attributeRepository->get( + 'catalog_product', + 'test_configurable_second' + ); + } + + return $this->secondConfigurableAttribute; + } + + /** + * Assert option uid. + * + * @param $attributeId + * @param $expectedOptions + * @param $selectedOptions + */ + private function assertAvailableOptionUids($attributeId, $expectedOptions, $selectedOptions): void + { + unset($expectedOptions[0]); + foreach ($expectedOptions as $option) { + self::assertContains( + $this->selectionUidFormatter->encode((int)$attributeId, (int)$option->getValue()), + $selectedOptions + ); + } + } + + /** + * Make fulltext catalog search reindex + * + * @return void + * @throws \Throwable + */ + private function reindexAll(): void + { + $indexLists = [ + 'catalog_category_product', + 'catalog_product_attribute', + 'cataloginventory_stock', + 'catalogsearch_fulltext', + ]; + + foreach ($indexLists as $indexerId) { + $indexer = $this->indexerFactory->create(); + $indexer->load($indexerId)->reindexAll(); + } + } + + /** + * Retrieve options UIDs + * + * @param array $options + * @return array + */ + private function getOptionsUids(array $options): array + { + $uids = []; + foreach ($options as $option) { + $uids[] = $option['uid']; + } + return $uids; + } +} From f2d7be34e5b64266af4925387bbed9526238432e Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 12 Jan 2021 17:20:55 +0200 Subject: [PATCH 013/137] magento/magento2#29549: Scheduled price rule time zone correction - test coverage. --- .../Unit/Model/Indexer/IndexBuilderTest.php | 195 ------------------ .../Model/Indexer/IndexerBuilderTest.php | 39 ++++ .../Model/Indexer/Product/PriceTest.php | 4 + ...h_catalog_rule_50_percent_off_tomorrow.php | 76 +++++++ ..._rule_50_percent_off_tomorrow_rollback.php | 55 +++++ .../_files/second_website_with_two_stores.php | 16 +- ...econd_website_with_two_stores_rollback.php | 8 - 7 files changed, 176 insertions(+), 217 deletions(-) delete mode 100644 app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow_rollback.php diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php deleted file mode 100644 index d4e64a684e4e1..0000000000000 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php +++ /dev/null @@ -1,195 +0,0 @@ -ruleCollectionFactoryMock = $this->createMock(CollectionFactory::class); - $this->priceCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class) - ->getMockForAbstractClass(); - $this->resourceMock = $this->createMock(ResourceConnection::class); - $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) - ->getMockForAbstractClass(); - $this->resourceMock->expects($this->once())->method('getConnection') - ->willReturn($this->connectionMock); - $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) - ->getMockForAbstractClass(); - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) - ->getMockForAbstractClass(); - $this->eavConfigMock = $this->createMock(Config::class); - $this->dateFormatMock = $this->createMock(DateTime::class); - $this->dateTimeMock = $this->createMock(DateTime\DateTime::class); - $this->productFactoryMock = $this->createMock(ProductFactory::class); - $this->batchCountMock = 99; - $this->reindexRuleProductMock = $this->createMock(ReindexRuleProduct::class); - $this->reindexRuleProductPriceMock = $this->createMock(ReindexRuleProductPrice::class); - - $this->indexBuilder = $objectManager->getObject( - IndexBuilder::class, - [ - 'ruleCollectionFactory' => $this->ruleCollectionFactoryMock, - 'priceCurrency' => $this->priceCurrencyMock, - 'resource' => $this->resourceMock, - 'storeManager' => $this->storeManagerMock, - 'logger' => $this->loggerMock, - 'eavConfig' => $this->eavConfigMock, - 'dateFormat' => $this->dateFormatMock, - 'dateTime' => $this->dateTimeMock, - 'productFactory' => $this->productFactoryMock, - 'batchCount' => $this->batchCountMock, - 'reindexRuleProduct' => $this->reindexRuleProductMock, - 'reindexRuleProductPrice' => $this->reindexRuleProductPriceMock, - ] - ); - } - - /** - * Test for \Magento\CatalogRule\Model\Indexer\IndexBuilder::reindexByIds. - * - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testReindexByIds() - { - $id1 = 1; - $id2 = 1; - $ids = [$id1, $id2]; - $collectionMock = $this->createMock(Collection::class); - $collectionMock->expects($this->once())->method('addFieldToFilter')->with('is_active', 1) - ->willReturn($collectionMock); - $ruleMock = $this->createMock(Rule::class); - $ruleMock->expects($this->once())->method('setProductsFilter')->with($ids); - $collectionMock->expects($this->once())->method('getItems')->willReturn([$ruleMock]); - $this->ruleCollectionFactoryMock->expects($this->once())->method('create') - ->willReturn($collectionMock); - $this->reindexRuleProductMock->expects($this->once())->method('execute') - ->with($ruleMock, $this->batchCountMock); - $this->reindexRuleProductPriceMock->expects($this->exactly(2))->method('execute') - ->withConsecutive([$this->batchCountMock, $id1], [$this->batchCountMock, $id2]); - - $this->indexBuilder->reindexByIds($ids); - } - - /** - * Test for \Magento\CatalogRule\Model\Indexer\IndexBuilder::reindexById. - * - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testReindexById() - { - $id = 1; - $collectionMock = $this->createMock(Collection::class); - $collectionMock->expects($this->once())->method('addFieldToFilter')->with('is_active', 1) - ->willReturn($collectionMock); - $ruleMock = $this->createMock(Rule::class); - $ruleMock->expects($this->once())->method('setProductsFilter')->with([$id]); - $collectionMock->expects($this->once())->method('getItems')->willReturn([$ruleMock]); - $this->ruleCollectionFactoryMock->expects($this->once())->method('create') - ->willReturn($collectionMock); - $this->reindexRuleProductMock->expects($this->once())->method('execute') - ->with($ruleMock, $this->batchCountMock); - $this->reindexRuleProductPriceMock->expects($this->once())->method('execute') - ->with($this->batchCountMock, $id); - - $this->indexBuilder->reindexById($id); - } -} diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php index 313ceca053591..d051822a89606 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogRule\Model\Indexer; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; class IndexerBuilderTest extends \PHPUnit\Framework\TestCase @@ -34,6 +36,16 @@ class IndexerBuilderTest extends \PHPUnit\Framework\TestCase */ protected $productThird; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + protected function setUp(): void { $this->indexerBuilder = Bootstrap::getObjectManager()->get( @@ -41,6 +53,8 @@ protected function setUp(): void ); $this->resourceRule = Bootstrap::getObjectManager()->get(\Magento\CatalogRule\Model\ResourceModel\Rule::class); $this->product = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Product::class); + $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); } protected function tearDown(): void @@ -82,6 +96,31 @@ public function testReindexById() $this->assertEquals(9.8, $this->resourceRule->getRulePrice(new \DateTime(), 1, 1, $product->getId())); } + /** + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow.php + * @magentoConfigFixture base_website general/locale/timezone Europe/Amsterdam + * @magentoConfigFixture general/locale/timezone America/Chicago + */ + public function testReindexByIdDifferentTimezones() + { + $productId = $this->productRepository->get('simple')->getId(); + $this->indexerBuilder->reindexById($productId); + + $mainWebsiteId = $this->storeManager->getWebsite('base')->getId(); + $secondWebsiteId = $this->storeManager->getWebsite('test')->getId(); + $rawTimestamp = (new \DateTime('+1 day'))->getTimestamp(); + $timestamp = $rawTimestamp - ($rawTimestamp % (60 * 60 * 24)); + $mainWebsiteActiveRules = + $this->resourceRule->getRulesFromProduct($timestamp, $mainWebsiteId, 1, $productId); + $secondWebsiteActiveRules = + $this->resourceRule->getRulesFromProduct($timestamp, $secondWebsiteId, 1, $productId); + + $this->assertCount(1, $mainWebsiteActiveRules); + $this->assertCount(0, $secondWebsiteActiveRules); + } + /** * @magentoDbIsolation disabled * @magentoAppIsolation enabled diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php index 4dc07309ee70b..716f8d6260c4a 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php @@ -96,7 +96,11 @@ public function testPriceForSecondStore():void { $websiteId = $this->websiteRepository->get('test')->getId(); $simpleProduct = $this->productRepository->get('simple'); + $simpleProduct->setPriceCalculation(true); $this->assertEquals('simple', $simpleProduct->getSku()); + $this->assertFalse( + $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()) + ); $this->indexerBuilder->reindexById($simpleProduct->getId()); $this->assertEquals( 25, diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow.php new file mode 100644 index 0000000000000..0b7e3e0b4e666 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow.php @@ -0,0 +1,76 @@ +requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +/** @var IndexBuilder $indexBuilder */ +$indexBuilder = $objectManager->get(IndexBuilder::class); +/** @var CatalogRuleRepositoryInterface $catalogRuleRepository */ +$catalogRuleRepository = $objectManager->get(CatalogRuleRepositoryInterface::class); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); + +$secondWebsite = $websiteRepository->get('test'); +$product = $productFactory->create(); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1, $secondWebsite->getId()]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(50) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ); +$productRepository->save($product); +/** @var Rule $rule */ +$catalogRule = $ruleFactory->create(); +$catalogRule->loadPost( + [ + 'name' => 'Test Catalog Rule 50% off tomorrow', + 'is_active' => '1', + 'stop_rules_processing' => 0, + 'website_ids' => [1, $secondWebsite->getId()], + 'customer_group_ids' => [Group::NOT_LOGGED_IN_ID, 1], + 'discount_amount' => 50, + 'simple_action' => 'by_percent', + 'from_date' => (new \DateTime('+1 day'))->format('m/d/Y'), + 'to_date' => '', + 'sort_order' => 0, + 'sub_is_enable' => 0, + 'sub_discount_amount' => 0, + 'conditions' => [], + ] +); +$catalogRuleRepository->save($catalogRule); +$indexBuilder->reindexFull(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow_rollback.php new file mode 100644 index 0000000000000..4745fc4d6772f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow_rollback.php @@ -0,0 +1,55 @@ +get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var CatalogRuleRepositoryInterface $ruleRepository */ +$ruleRepository = $objectManager->get(CatalogRuleRepositoryInterface::class); +/** @var IndexBuilder $indexBuilder */ +$indexBuilder = $objectManager->get(IndexBuilder::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productRepository->deleteById('simple'); +} catch (NoSuchEntityException $e) { + //already removed +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +/** @var Rule $catalogRuleResource */ +$catalogRuleResource = $objectManager->create(Rule::class); +//Retrieve rule by name +$select = $catalogRuleResource->getConnection() + ->select() + ->from($catalogRuleResource->getMainTable(), 'rule_id') + ->where('name = ?', 'Test Catalog Rule 50% off tomorrow'); +$ruleId = $catalogRuleResource->getConnection()->fetchOne($select); + +try { + $ruleRepository->deleteById($ruleId); +} catch (CouldNotDeleteException $ex) { + //Nothing to remove +} + +$indexBuilder->reindexFull(); + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php index 47255f67ddbf0..016acca1e8e04 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php @@ -10,23 +10,11 @@ $website->save(); } $websiteId = $website->getId(); - -$storeGroup = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Group::class); -if (!$storeGroup->load('fixture_second_store_group', 'code')->getId()) { - $storeGroup->setCode('fixture_second_store_group') - ->setName('Fixture Second Store Group') - ->setWebsite($website); - $storeGroup->save(); - - $website->setDefaultGroupId($storeGroup->getId()); - $website->save(); -} - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); if (!$store->load('fixture_second_store', 'code')->getId()) { $groupId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class - )->getWebsite('test')->getDefaultGroupId(); + )->getWebsite()->getDefaultGroupId(); $store->setCode( 'fixture_second_store' )->setWebsiteId( @@ -47,7 +35,7 @@ if (!$store->load('fixture_third_store', 'code')->getId()) { $groupId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class - )->getWebsite('test')->getDefaultGroupId(); + )->getWebsite()->getDefaultGroupId(); $store->setCode( 'fixture_third_store' )->setWebsiteId( diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php index 2b2a86ad55931..eef8cf960944c 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php @@ -15,14 +15,6 @@ if ($websiteId) { $website->delete(); } - -$storeGroup = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Group::class); -/** @var $storeGroup \Magento\Store\Model\Group */ -$storeGroupId = $storeGroup->load('fixture_second_store_group', 'code')->getId(); -if ($storeGroupId) { - $storeGroup->delete(); -} - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); if ($store->load('fixture_second_store', 'code')->getId()) { $store->delete(); From 6831e5213535c4393c9863cd121e9730d871a39f Mon Sep 17 00:00:00 2001 From: Oleksandr Melnyk Date: Tue, 12 Jan 2021 19:13:24 +0200 Subject: [PATCH 014/137] magento/magento2#31332:Shema changes implementation for configurable product option selection - added tests for media gallery --- .../ConfigurableOptionsSelectionTest.php | 36 +++++++++++ ...oducts_with_two_attributes_combination.php | 63 ++++++++++++++----- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php index dc5817b7a15ff..7ea02a0fa1a45 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php @@ -12,6 +12,7 @@ use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\AttributeRepository; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Indexer\Model\IndexerFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -36,6 +37,11 @@ class ConfigurableOptionsSelectionTest extends GraphQlAbstract */ private $indexerFactory; + /** + * @var Uid + */ + private $idEncoder; + private $firstConfigurableAttribute; private $secondConfigurableAttribute; @@ -48,6 +54,7 @@ protected function setUp(): void $this->attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); $this->selectionUidFormatter = Bootstrap::getObjectManager()->create(SelectionUidFormatter::class); $this->indexerFactory = Bootstrap::getObjectManager()->create(IndexerFactory::class); + $this->idEncoder = Bootstrap::getObjectManager()->create(Uid::class); } /** @@ -85,6 +92,8 @@ public function testSelectedFirstAttributeFirstOption(): void $product['configurable_product_options_selection']['configurable_options'][0]['values'] ) ); + + $this->assertMediaGallery($product); } /** @@ -120,6 +129,18 @@ public function testSelectedVariant(): void self::assertEquals($sku, $product['sku']); self::assertEmpty($product['configurable_product_options_selection']['configurable_options']); self::assertNotNull($product['configurable_product_options_selection']['variant']); + + $variantId = $this->idEncoder->decode($product['configurable_product_options_selection']['variant']['uid']); + self::assertIsNumeric($variantId); + self::assertIsString($product['configurable_product_options_selection']['variant']['sku']); + $urlKey = 'configurable-option-first-option-1-second-option-1'; + self::assertEquals($urlKey, $product['configurable_product_options_selection']['variant']['url_key']); + self::assertMatchesRegularExpression( + "/{$urlKey}/", + $product['configurable_product_options_selection']['variant']['url_path'] + ); + + $this->assertMediaGallery($product); } /** @@ -161,6 +182,8 @@ public function testWithoutSelectedOption(): void $product['configurable_product_options_selection']['configurable_options'][1]['values'] ) ); + + $this->assertMediaGallery($product); } /** @@ -345,4 +368,17 @@ private function getOptionsUids(array $options): array } return $uids; } + + /** + * Assert media gallery fields + * + * @param array $product + */ + private function assertMediaGallery(array $product): void + { + self::assertNotEmpty($product['configurable_product_options_selection']['media_gallery']); + $image = current($product['configurable_product_options_selection']['media_gallery']); + self::assertIsString($image['url']); + self::assertEquals(false, $image['disabled']); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php index 24e6010275bac..f26e39ca8e2a2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Media\Config; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Setup\CategorySetup; @@ -15,8 +17,11 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Framework\App\Filesystem\DirectoryList; Resolver::getInstance()->requireDataFixture( 'Magento/ConfigurableProduct/_files/configurable_attribute_first.php' @@ -25,18 +30,31 @@ 'Magento/ConfigurableProduct/_files/configurable_attribute_second.php' ); +$objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ -$productRepository = Bootstrap::getObjectManager() - ->get(ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); /** @var $installer CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$installer = $objectManager->create(CategorySetup::class); /** @var \Magento\Eav\Model\Config $eavConfig */ -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); $firstAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); $secondAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_second'); +/** @var Config $config */ +$config = $objectManager->get(Config::class); + +/** @var Filesystem $filesystem */ +$filesystem = $objectManager->get(Filesystem::class); + +/** @var WriteInterface $mediaDirectory */ +$mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); +$mediaPath = $mediaDirectory->getAbsolutePath(); +$baseTmpMediaPath = $config->getBaseTmpMediaPath(); +$mediaDirectory->create($baseTmpMediaPath); + /* Create simple products per each option value*/ /** @var AttributeOptionInterface[] $firstAttributeOptions */ $firstAttributeOptions = $firstAttribute->getOptions(); @@ -48,6 +66,8 @@ $firstAttributeValues = []; $secondAttributeValues = []; $testImagePath = __DIR__ . '/magento_image.jpg'; +$mediaImage = $mediaPath . '/' . $baseTmpMediaPath . '/magento_image.jpg'; +copy($testImagePath, $mediaImage); array_shift($firstAttributeOptions); array_shift($secondAttributeOptions); @@ -65,7 +85,10 @@ $qty = 100; $isInStock = 1; } - $product = Bootstrap::getObjectManager()->create(Product::class); + + $image = '/m/a/magento_image.jpg'; + + $product = $objectManager->create(Product::class); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) @@ -79,15 +102,15 @@ ->setStockData( ['use_config_manage_stock' => 1, 'qty' => $qty, 'is_qty_decimal' => 0, 'is_in_stock' => $isInStock] ) - ->setImage('/m/a/magento_image.jpg') - ->setSmallImage('/m/a/magento_image.jpg') - ->setThumbnail('/m/a/magento_image.jpg') + ->setImage($image) + ->setSmallImage($image) + ->setThumbnail($image) ->setData( 'media_gallery', [ 'images' => [ [ - 'file' => '/m/a/magento_image.jpg', + 'file' => $image, 'position' => 1, 'label' => 'Image Alt Text', 'disabled' => 0, @@ -113,11 +136,12 @@ foreach ($customAttributes as $attributeCode => $attributeValue) { $product->setCustomAttributes($customAttributes); } + $product = $productRepository->save($product); $associatedProductIds[] = $product->getId(); - /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ - $stockItem = Bootstrap::getObjectManager()->create(Item::class); + /** @var Item $stockItem */ + $stockItem = $objectManager->create(Item::class); $stockItem->load($product->getId(), 'product_id'); if (!$stockItem->getProductId()) { @@ -135,17 +159,16 @@ 'value_index' => $secondAttributeOption->getValue(), ]; } - } -$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor = $objectManager->get(PriceIndexerProcessor::class); $indexerProcessor->reindexList($associatedProductIds, true); /** @var $product Product */ -$product = Bootstrap::getObjectManager()->create(Product::class); +$product = $objectManager->create(Product::class); /** @var Factory $optionsFactory */ -$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$optionsFactory = $objectManager->create(Factory::class); $configurableAttributesData = [ [ @@ -180,9 +203,15 @@ ->setSku('configurable_12345') ->setVisibility(Visibility::VISIBILITY_BOTH) ->setStatus(Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->addImageToMediaGallery( + $mediaImage, + ['image', 'small_image', 'thumbnail'], + false, + false + ); $productRepository->cleanCache(); $product = $productRepository->save($product); -$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor = $objectManager->get(PriceIndexerProcessor::class); $indexerProcessor->reindexRow($product->getId(), true); From b339166faec5fd9832ecf3e05b6d2cbebe343458 Mon Sep 17 00:00:00 2001 From: Oleksandr Melnyk Date: Wed, 13 Jan 2021 12:12:38 +0200 Subject: [PATCH 015/137] magento/magento2#31332:Schema changes implementation for configurable option selection - fixed webapi tests --- .../Model/Options/SelectionUidFormatter.php | 6 +- ...gurableProductToCartSingleMutationTest.php | 86 +++++++++++-------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php index 7bcf08a9b9509..8c82c9414763f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php @@ -46,11 +46,7 @@ public function __construct(Uid $idEncoder) */ public function encode(int $attributeId, int $indexId): string { - return $this->idEncoder->encode(implode(self::UID_SEPARATOR, [ - self::UID_PREFIX, - $attributeId, - $indexId - ])); + return $this->idEncoder->encode(implode(self::UID_SEPARATOR, [self::UID_PREFIX, $attributeId, $indexId])); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php index 5a08692d5dcdd..b26e5b4139d0f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -3,18 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\GraphQl\ConfigurableProduct; use Exception; +use Magento\CatalogInventory\Model\Configuration; use Magento\Config\Model\ResourceModel\Config; +use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\CatalogInventory\Model\Configuration; /** * Add configurable product to cart testcases @@ -41,6 +43,11 @@ class AddConfigurableProductToCartSingleMutationTest extends GraphQlAbstract */ private $reinitConfig; + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + /** * @inheritdoc */ @@ -51,13 +58,14 @@ protected function setUp(): void $this->resourceConfig = $objectManager->get(Config::class); $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->selectionUidFormatter = $objectManager->get(SelectionUidFormatter::class); } /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddConfigurableProductToCart() + public function testAddConfigurableProductToCart(): void { $product = $this->getConfigurableProductInfo(); $quantity = 2; @@ -77,7 +85,8 @@ public function testAddConfigurableProductToCart() ); $response = $this->graphQlMutation($query); - $expectedProductOptionsValueUid = $this->generateConfigurableSelectionUID($attributeId, $valueIndex); + + $expectedProductOptionsValueUid = $this->selectionUidFormatter->encode($attributeId, $valueIndex); $expectedProductOptionsUid = base64_encode("configurable/$productRowId/$attributeId"); $cartItem = current($response['addProductsToCart']['cart']['items']); self::assertEquals($quantity, $cartItem['quantity']); @@ -94,35 +103,11 @@ public function testAddConfigurableProductToCart() self::assertArrayHasKey('value_label', $option); } - /** - * Generates UID configurable product - * - * @param int $attributeId - * @param int $valueIndex - * @return string - */ - private function generateConfigurableSelectionUID(int $attributeId, int $valueIndex): string - { - return base64_encode("configurable/$attributeId/$valueIndex"); - } - - /** - * Generates UID for super configurable product super attributes - * - * @param int $attributeId - * @param int $valueIndex - * @return string - */ - private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string - { - return 'selected_options: ["' . $this->generateConfigurableSelectionUID($attributeId, $valueIndex) . '"]'; - } - /** * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddConfigurableProductWithWrongSuperAttributes() + public function testAddConfigurableProductWithWrongSuperAttributes(): void { $product = $this->getConfigurableProductInfo(); $quantity = 2; @@ -150,7 +135,7 @@ public function testAddConfigurableProductWithWrongSuperAttributes() * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddProductIfQuantityIsNotAvailable() + public function testAddProductIfQuantityIsNotAvailable(): void { $product = $this->getConfigurableProductInfo(); $parentSku = $product['sku']; @@ -179,7 +164,7 @@ public function testAddProductIfQuantityIsNotAvailable() * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddNonExistentConfigurableProductParentToCart() + public function testAddNonExistentConfigurableProductParentToCart(): void { $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $parentSku = 'configurable_no_exist'; @@ -203,7 +188,7 @@ public function testAddNonExistentConfigurableProductParentToCart() * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_zero_qty_first_child.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testOutOfStockVariationToCart() + public function testOutOfStockVariationToCart(): void { $showOutOfStock = $this->scopeConfig->getValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK); @@ -215,7 +200,7 @@ public function testOutOfStockVariationToCart() $attributeId = (int) $product['configurable_options'][0]['attribute_id']; $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; // Asserting that the first value is the right option we want to add to cart - $this->assertEquals( + self::assertEquals( $product['configurable_options'][0]['values'][0]['label'], 'Option 1' ); @@ -237,7 +222,7 @@ public function testOutOfStockVariationToCart() 'There are no source items with the in stock status', 'This product is out of stock.' ]; - $this->assertContains( + self::assertContains( $response['addProductsToCart']['user_errors'][0]['message'], $expectedErrorMessages ); @@ -312,6 +297,18 @@ private function getConfigurableProductInfo(): array return current($searchResponse['products']['items']); } + /** + * Generates UID for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * @return string + */ + private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . $this->selectionUidFormatter->encode($attributeId, $valueIndex) . '"]'; + } + /** * Returns GraphQl query for fetching configurable product information * @@ -349,15 +346,28 @@ private function getFetchProductQuery(string $term): string value_index } } - configurable_options_selection_metadata { - options_available_for_selection { + configurable_product_options_selection { + configurable_options { + uid attribute_code - option_value_uids + label + values { + uid + is_available + is_use_default + label + } } variant { uid - name - attribute_set_id + sku + url_key + url_path + } + media_gallery { + url + label + disabled } } } From d6b3c40c965f5eb11ec9ddc70491228989dca602 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Wed, 13 Jan 2021 13:53:40 +0200 Subject: [PATCH 016/137] magento/magento2#29549: Scheduled price rule time zone correction - test update. --- .../Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php index d051822a89606..43e8fa88a4e94 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php @@ -118,7 +118,10 @@ public function testReindexByIdDifferentTimezones() $this->resourceRule->getRulesFromProduct($timestamp, $secondWebsiteId, 1, $productId); $this->assertCount(1, $mainWebsiteActiveRules); - $this->assertCount(0, $secondWebsiteActiveRules); + // Avoid failure then staging is enabled as it removes catalog rule timestamp. + if ((int)$mainWebsiteActiveRules[0]['from_time'] !== 0) { + $this->assertCount(0, $secondWebsiteActiveRules); + } } /** From fa821da8212a8f838ab0c657117a4dc27fb9b415 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Wed, 13 Jan 2021 13:54:35 +0200 Subject: [PATCH 017/137] magento/magento2#29549: Scheduled price rule time zone correction - test update. --- .../Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php index 43e8fa88a4e94..b51284c42ce90 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php @@ -118,7 +118,7 @@ public function testReindexByIdDifferentTimezones() $this->resourceRule->getRulesFromProduct($timestamp, $secondWebsiteId, 1, $productId); $this->assertCount(1, $mainWebsiteActiveRules); - // Avoid failure then staging is enabled as it removes catalog rule timestamp. + // Avoid failure when staging is enabled as it removes catalog rule timestamp. if ((int)$mainWebsiteActiveRules[0]['from_time'] !== 0) { $this->assertCount(0, $secondWebsiteActiveRules); } From 30db2dc472fc3dfa82f21c77e41fee74f851d57f Mon Sep 17 00:00:00 2001 From: ogorkun Date: Tue, 19 Jan 2021 18:31:12 -0600 Subject: [PATCH 018/137] MC-38539: Introduce JWT wrapper --- app/etc/di.xml | 1 + composer.json | 3 +- .../Magento/Framework/Jwt/JwtManagerTest.php | 61 ++++++ .../Magento/Framework/Jwt/ClaimInterface.php | 51 +++++ .../Framework/Jwt/ClaimsPayloadInterface.php | 22 ++ .../Jwt/EncryptionSettingsInterface.php | 22 ++ .../Jwt/Exception/EncryptionException.php | 17 ++ .../Jwt/Exception/ExpiredException.php | 75 +++++++ .../Framework/Jwt/Exception/JwtException.php | 17 ++ .../Jwt/Exception/MalformedTokenException.php | 17 ++ .../Magento/Framework/Jwt/HeaderInterface.php | 22 ++ .../Jwt/HeaderParameterInterface.php | 42 ++++ .../Framework/Jwt/Jwe/JweInterface.php | 41 ++++ lib/internal/Magento/Framework/Jwt/Jwk.php | 206 ++++++++++++++++++ lib/internal/Magento/Framework/Jwt/JwkSet.php | 36 +++ .../Magento/Framework/Jwt/Jws/Jws.php | 84 +++++++ .../Magento/Framework/Jwt/Jws/JwsHeader.php | 42 ++++ .../Jwt/Jws/JwsHeaderParameterInterface.php | 17 ++ .../Framework/Jwt/Jws/JwsInterface.php | 34 +++ .../Framework/Jwt/Jws/JwsSignatureJwks.php | 76 +++++++ .../Jwt/Jws/JwsSignatureSettingsInterface.php | 19 ++ .../Jwt/JwtFrameworkAdapter/JwtManager.php | 203 +++++++++++++++++ .../Magento/Framework/Jwt/JwtInterface.php | 29 +++ .../Framework/Jwt/JwtManagerInterface.php | 37 ++++ .../Framework/Jwt/NestedPayloadInterface.php | 22 ++ .../Framework/Jwt/PayloadInterface.php | 22 ++ .../Framework/Jwt/PrivateHeaderParameter.php | 58 +++++ .../Framework/Jwt/Unsecured/NoEncryption.php | 25 +++ .../Jwt/Unsecured/UnsecuredJwtInterface.php | 19 ++ lib/internal/Magento/Framework/composer.json | 3 +- 30 files changed, 1321 insertions(+), 2 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php create mode 100644 lib/internal/Magento/Framework/Jwt/ClaimInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/ClaimsPayloadInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/EncryptionSettingsInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/Exception/EncryptionException.php create mode 100644 lib/internal/Magento/Framework/Jwt/Exception/ExpiredException.php create mode 100644 lib/internal/Magento/Framework/Jwt/Exception/JwtException.php create mode 100644 lib/internal/Magento/Framework/Jwt/Exception/MalformedTokenException.php create mode 100644 lib/internal/Magento/Framework/Jwt/HeaderInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/HeaderParameterInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/JweInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwk.php create mode 100644 lib/internal/Magento/Framework/Jwt/JwkSet.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/Jws.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/JwsHeaderParameterInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/JwsInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureJwks.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureSettingsInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/JwtFrameworkAdapter/JwtManager.php create mode 100644 lib/internal/Magento/Framework/Jwt/JwtInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/JwtManagerInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/NestedPayloadInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/PayloadInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/PrivateHeaderParameter.php create mode 100644 lib/internal/Magento/Framework/Jwt/Unsecured/NoEncryption.php create mode 100644 lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwtInterface.php diff --git a/app/etc/di.xml b/app/etc/di.xml index b1d81ed70f6b4..c7ad3014f067d 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1912,4 +1912,5 @@ + diff --git a/composer.json b/composer.json index 6aa9355cec7b1..a48c1c62e92c8 100644 --- a/composer.json +++ b/composer.json @@ -83,7 +83,8 @@ "wikimedia/less.php": "~1.8.0", "league/flysystem": "^1.0", "league/flysystem-aws-s3-v3": "^1.0", - "league/flysystem-cached-adapter": "^1.0" + "league/flysystem-cached-adapter": "^1.0", + "web-token/jwt-framework": "^v2.2.7" }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php new file mode 100644 index 0000000000000..dca31754b1995 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -0,0 +1,61 @@ +manager = $objectManager->get(JwtManagerInterface::class); + } + + /** + * Verify that the manager is able to create and read token data correctly. + * + * @param JwtInterface $jwt + * @param EncryptionSettingsInterface $encryption + * @return void + */ + public function testCreateRead(JwtInterface $jwt, EncryptionSettingsInterface $encryption): void + { + $recreated = $this->manager->read($this->manager->create($jwt, $encryption), [$encryption]); + if ($jwt instanceof JwsInterface) { + $this->assertInstanceOf(JwsInterface::class, $recreated); + } + if ($jwt instanceof JweInterface) { + $this->assertInstanceOf(JweInterface::class, $recreated); + } + if ($jwt instanceof UnsecuredJwtInterface) { + $this->assertInstanceOf(UnsecuredJwtInterface::class, $recreated); + } + } + + public function getTokenVariants(): array + { + return [ + 'jws-HS256' => [ + + ] + ]; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/ClaimInterface.php b/lib/internal/Magento/Framework/Jwt/ClaimInterface.php new file mode 100644 index 0000000000000..739c1b46dc244 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/ClaimInterface.php @@ -0,0 +1,51 @@ +expiresAt = $expiresAt; + $this->activeFrom = $activeFrom; + $this->checked = time(); + } + + /** + * JWT will be active from. + * + * @return int|null + */ + public function getActiveFrom(): ?int + { + return $this->activeFrom; + } + + /** + * JWT expired at. + * + * @return int|null + */ + public function getExpiresAt(): ?int + { + return $this->expiresAt; + } + + /** + * Check was performed at. + * + * @return int + */ + public function getChecked(): int + { + return $this->checked; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Exception/JwtException.php b/lib/internal/Magento/Framework/Jwt/Exception/JwtException.php new file mode 100644 index 0000000000000..dc3f99ce8c53c --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Exception/JwtException.php @@ -0,0 +1,17 @@ +kty = $kty; + $this->data = $data; + $this->use = $use; + $this->keyOps = $keyOps; + $this->alg = $alg; + $this->x5u = $x5u; + $this->x5c = $x5c; + $this->x5t = $x5t; + $this->x5ts256 = $x5ts256; + } + + /** + * "kty" parameter. + * + * @return string + */ + public function getKeyType(): string + { + return $this->kty; + } + + /** + * "use" parameter. + * + * @return string|null + */ + public function getPublicKeyUse(): ?string + { + return $this->use; + } + + /** + * "key_ops" parameter. + * + * @return string[]|null + */ + public function getKeyOperations(): ?array + { + return $this->keyOps; + } + + /** + * "alg" parameter. + * + * @return string|null + */ + public function getAlgorithm(): ?string + { + return $this->alg; + } + + /** + * "x5u" parameter. + * + * @return string|null + */ + public function getX509Url(): ?string + { + return $this->x5u; + } + + /** + * "x5c" parameter. + * + * @return string[]|null + */ + public function getX509CertificateChain(): ?array + { + return $this->x5c; + } + + /** + * "x5t" parameter. + * + * @return string|null + */ + public function getX509Sha1Thumbprint(): ?string + { + return $this->x5t; + } + + /** + * "x5t#S256" parameter. + * + * @return string|null + */ + public function getX509Sha256Thumbprint(): ?string + { + return $this->x5ts256; + } + + /** + * Map with algorithm (type) specific data. + * + * @return string[] + */ + public function getAlgoData(): array + { + return $this->data; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/JwkSet.php b/lib/internal/Magento/Framework/Jwt/JwkSet.php new file mode 100644 index 0000000000000..93a1c0f8c90b4 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/JwkSet.php @@ -0,0 +1,36 @@ +keys = $keys; + } + + /** + * @return Jwk[] + */ + public function getKeys(): array + { + return $this->keys; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jws/Jws.php b/lib/internal/Magento/Framework/Jwt/Jws/Jws.php new file mode 100644 index 0000000000000..9856fcfb014b5 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jws/Jws.php @@ -0,0 +1,84 @@ +protectedHeaders = array_values($protectedHeaders); + $this->payload = $payload; + if (!$unprotectedHeaders) { + $unprotectedHeaders = null; + } elseif (count($protectedHeaders) !== count($unprotectedHeaders)) { + throw new \InvalidArgumentException('There has to be equal amount of protected and unprotected headers'); + } else { + $unprotectedHeaders = array_values($unprotectedHeaders); + } + $this->unprotectedHeaders = $unprotectedHeaders; + } + + /** + * @inheritDoc + */ + public function getProtectedHeaders(): array + { + return $this->protectedHeaders; + } + + /** + * @inheritDoc + */ + public function getUnprotectedHeaders(): ?array + { + return $this->unprotectedHeaders; + } + + /** + * @inheritDoc + */ + public function getHeader(): HeaderInterface + { + return $this->protectedHeaders[0]; + } + + /** + * @inheritDoc + */ + public function getPayload(): PayloadInterface + { + return $this->payload; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php b/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php new file mode 100644 index 0000000000000..ea531cb85381c --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php @@ -0,0 +1,42 @@ +getName()) + ); + } + } + $this->parameters = $parameters; + } + + /** + * @inheritDoc + */ + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jws/JwsHeaderParameterInterface.php b/lib/internal/Magento/Framework/Jwt/Jws/JwsHeaderParameterInterface.php new file mode 100644 index 0000000000000..9967592a9fdab --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jws/JwsHeaderParameterInterface.php @@ -0,0 +1,17 @@ +jwkSet = $jwk; + foreach ($this->jwkSet->getKeys() as $jwk) { + $this->validateJwk($jwk); + } + } + + /** + * @inheritDoc + */ + public function getAlgorithmName(): string + { + if (count($this->jwkSet->getKeys()) > 1) { + return 'jws-json-serialization'; + } else { + return $this->jwkSet->getKeys()[0]->getAlgorithm(); + } + } + + /** + * JWK Set. + * + * @return JwkSet + */ + public function getJwkSet(): JwkSet + { + return $this->jwkSet; + } + + /** + * Validate JWK values. + * + * @param Jwk $jwk + * @return void + */ + private function validateJwk(Jwk $jwk): void + { + if ($jwk->getPublicKeyUse() === Jwk::PUBLIC_KEY_USE_ENCRYPTION) { + throw new EncryptionException('JWK is meant for JWEs'); + } + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureSettingsInterface.php b/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureSettingsInterface.php new file mode 100644 index 0000000000000..4fc8f20d79a44 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureSettingsInterface.php @@ -0,0 +1,19 @@ +jwsBuilder = new JWSBuilder( + new AlgorithmManager((new AlgorithmProvider($jwsAlgorithms))->getAvailableAlgorithms()) + ); + $this->jwsCompactSerializer = new JwsCompactSerializer(); + $this->jwsJsonSerializer = new JwsJsonSerializer(); + $this->jwsFlatSerializer = new JwsFlatSerializer(); + } + + /** + * @inheritDoc + */ + public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryption): string + { + if (!$jwt instanceof UnsecuredJwtInterface && !$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) { + throw new MalformedTokenException('Can only build JWS, JWE or Unsecured tokens.'); + } + if ($jwt instanceof JwsInterface) { + return $this->buildJws($jwt, $encryption); + } + } + + /** + * @inheritDoc + */ + public function read(string $token, array $acceptableEncryption): JwtInterface + { + // TODO: Implement read() method. + } + + /** + * Convert JWK. + * + * @param Jwk $jwk + * @return AdapterJwk + */ + private function convertToAdapterJwk(Jwk $jwk): AdapterJwk + { + $data = [ + 'kty' => $jwk->getKeyType(), + 'use' => $jwk->getPublicKeyUse(), + 'key_ops' => $jwk->getKeyOperations(), + 'alg' => $jwk->getAlgorithm(), + 'x5u' => $jwk->getX509Url(), + 'x5c' => $jwk->getX509CertificateChain(), + 'x5t' => $jwk->getX509Sha1Thumbprint(), + 'x5t#S256' => $jwk->getX509Sha256Thumbprint() + ]; + + return new AdapterJwk(array_merge($data, $jwk->getAlgoData())); + } + + /** + * Extract JOSE header data. + * + * @param HeaderInterface $header + * @return array + */ + private function extractHeaderData(HeaderInterface $header): array + { + $data = []; + foreach ($header->getParameters() as $parameter) { + $data[$parameter->getName()] = $parameter->getValue(); + } + + return $data; + } + + /** + * Create a JWS. + * + * @param JwsInterface $jws + * @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings + * @return string + * @throws JwtException + */ + private function buildJws(JwsInterface $jws, EncryptionSettingsInterface $encryptionSettings): string + { + if (!$encryptionSettings instanceof JwsSignatureJwks) { + throw new JwtException('Can only work with JWK settings for JWS tokens'); + } + $signaturesCount = count($encryptionSettings->getJwkSet()->getKeys()); + if ($jws->getProtectedHeaders() && count($jws->getProtectedHeaders()) !== $signaturesCount) { + throw new MalformedTokenException('Number of headers must equal to number of JWKs'); + } + if ($jws->getUnprotectedHeaders() + && count($jws->getUnprotectedHeaders()) !== $signaturesCount + ) { + throw new MalformedTokenException('There must be an equal number of protected and unprotected headers.'); + } + + try { + $builder = $this->jwsBuilder->create(); + $builder = $builder->withPayload($jws->getPayload()->getContent()); + for ($i = 0; $i <= $signaturesCount; $i++) { + $jwk = $encryptionSettings->getJwkSet()->getKeys()[$i]; + $alg = $jwk->getAlgorithm(); + if (!$alg) { + throw new EncryptionException('Algorithm is required for JWKs'); + } + $protected = []; + if ($jws->getProtectedHeaders()) { + $protected = $this->extractHeaderData($jws->getProtectedHeaders()[$i]); + } + $protected['alg'] = $alg; + $unprotected = []; + if ($jws->getUnprotectedHeaders()) { + $unprotected = $this->extractHeaderData($jws->getUnprotectedHeaders()[$i]); + $unprotected['alg'] = $alg; + } + $builder = $builder->addSignature($this->convertToAdapterJwk($jwk), $protected, $unprotected); + } + $jwsCreated = $builder->build(); + + if ($signaturesCount > 1) { + return $this->jwsJsonSerializer->serialize($jwsCreated); + } + if ($jws->getUnprotectedHeaders()) { + return $this->jwsFlatSerializer->serialize($jwsCreated); + } + return $this->jwsCompactSerializer->serialize($jwsCreated); + } catch (\Throwable $exception) { + if (!$exception instanceof JwtException) { + $exception = new JwtException('Something went wrong while generating a JWS', 0, $exception); + } + throw $exception; + } + } +} diff --git a/lib/internal/Magento/Framework/Jwt/JwtInterface.php b/lib/internal/Magento/Framework/Jwt/JwtInterface.php new file mode 100644 index 0000000000000..84e406a9452b9 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/JwtInterface.php @@ -0,0 +1,29 @@ +name = $name; + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_PRIVATE; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Unsecured/NoEncryption.php b/lib/internal/Magento/Framework/Jwt/Unsecured/NoEncryption.php new file mode 100644 index 0000000000000..59596e444145e --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Unsecured/NoEncryption.php @@ -0,0 +1,25 @@ + Date: Wed, 20 Jan 2021 13:38:23 +0200 Subject: [PATCH 019/137] fixed storeId param name --- app/code/Magento/Directory/Helper/Data.php | 23 +-- .../Directory/Test/Unit/Helper/DataTest.php | 160 +++--------------- 2 files changed, 34 insertions(+), 149 deletions(-) diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 123a338a343d2..2e89d2ecd7e13 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -190,8 +190,8 @@ public function getRegionJson() \Magento\Framework\Profiler::start('TEST: ' . __METHOD__, ['group' => 'TEST', 'method' => __METHOD__]); if (!$this->_regionJson) { $scope = $this->getCurrentScope(); - $scopeKey = $scope['value'] ? '_' . implode('_', $scope) : ''; - $cacheKey = 'DIRECTORY_REGIONS_JSON' . $scopeKey; + $scopeKey = $scope['value'] ? '_' . implode('_', $scope) : null; + $cacheKey = 'DIRECTORY_REGIONS_JSON_STORE' . $scopeKey; $json = $this->_configCacheType->load($cacheKey); if (empty($json)) { $regions = $this->getRegionData(); @@ -406,30 +406,23 @@ public function getWeightUnit() * Get current scope from request * * @return array - * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getCurrentScope(): array { - $scope = $this->_storeManager->getStore() - ? [ - 'type' => ScopeInterface::SCOPE_STORE, - 'value' => $this->_storeManager->getStore()->getId(), - ] - : [ - 'type' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 'value' => null, - ]; + $scope = [ + 'type' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 'value' => null, + ]; $request = $this->_getRequest(); - if ($request->getParam(ScopeInterface::SCOPE_WEBSITE)) { $scope = [ 'type' => ScopeInterface::SCOPE_WEBSITE, 'value' => $request->getParam(ScopeInterface::SCOPE_WEBSITE), ]; - } elseif ($request->getParam(ScopeInterface::SCOPE_STORE)) { + } elseif ($request->getParam('store_id')) { $scope = [ 'type' => ScopeInterface::SCOPE_STORE, - 'value' => $request->getParam(ScopeInterface::SCOPE_STORE), + 'value' => $request->getParam('store_id'), ]; } diff --git a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php index f9da2bdd6912a..2cb55a32b0772 100644 --- a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php @@ -7,7 +7,6 @@ namespace Magento\Directory\Test\Unit\Helper; -use ArrayIterator; use Magento\Directory\Helper\Data; use Magento\Directory\Model\AllowedCountries; use Magento\Directory\Model\CurrencyFactory; @@ -53,16 +52,6 @@ class DataTest extends TestCase */ protected $_store; - /** - * @var RequestInterface|MockObject - */ - protected $_request; - - /** - * @var StoreManagerInterface|MockObject - */ - protected $_storeManager; - /** * @var ScopeConfigInterface|MockObject */ @@ -78,10 +67,10 @@ protected function setUp(): void $objectManager = new ObjectManager($this); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(false); - $this->_request = $this->getMockForAbstractClass(RequestInterface::class); + $requestMock = $this->getMockForAbstractClass(RequestInterface::class); $context = $this->createMock(Context::class); $context->method('getRequest') - ->willReturn($this->_request); + ->willReturn($requestMock); $context->expects($this->any()) ->method('getScopeConfig') ->willReturn($this->scopeConfigMock); @@ -105,7 +94,8 @@ protected function setUp(): void $this->jsonHelperMock = $this->createMock(JsonDataHelper::class); $this->_store = $this->createMock(Store::class); - $this->_storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $storeManager->expects($this->any())->method('getStore')->willReturn($this->_store); $currencyFactory = $this->createMock(CurrencyFactory::class); @@ -115,103 +105,14 @@ protected function setUp(): void 'countryCollection' => $this->_countryCollection, 'regCollectionFactory' => $regCollectionFactory, 'jsonHelper' => $this->jsonHelperMock, - 'storeManager' => $this->_storeManager, + 'storeManager' => $storeManager, 'currencyFactory' => $currencyFactory, ]; $this->_object = $objectManager->getObject(Data::class, $arguments); } - /** - * @return array - */ - public function regionJsonProvider(): array - { - $countries = [ - 'Country1' => [ - 'r1' => ['code' => 'r1-code', 'name' => 'r1-name'], - 'r2' => ['code' => 'r2-code', 'name' => 'r2-name'] - ], - 'Country2' => [ - 'r3' => ['code' => 'r3-code', 'name' => 'r3-name'], - ], - 'Country3' => [], - ]; - - return [ - [ - null, - $countries, - ], - [ - null, - [ - 'Country1' => $countries['Country1'], - ], - [ScopeInterface::SCOPE_WEBSITE => 1], - ], - [ - 1, - [ - 'Country2' => $countries['Country2'], - ], - ], - [ - null, - [ - 'Country2' => $countries['Country2'], - ], - [ - ScopeInterface::SCOPE_WEBSITE => null, - ScopeInterface::SCOPE_STORE => 1, - ], - ], - [ - 2, - [ - 'Country3' => $countries['Country3'], - ], - ], - [ - null, - [ - 'Country3' => $countries['Country3'], - ], - [ScopeInterface::SCOPE_STORE => 2], - ], - ]; - } - - /** - * @param int|null $currentStoreId - * @param array $allowedCountries - * @param array $requestParams - * @dataProvider regionJsonProvider - */ - public function testGetRegionJson(?int $currentStoreId, array $allowedCountries, array $requestParams = []) + public function testGetRegionJson() { - if ($currentStoreId) { - $this->_store->method('getId')->willReturn($currentStoreId); - $this->_storeManager->expects($this->any())->method('getStore')->willReturn($this->_store); - } else { - $this->_storeManager->expects($this->any())->method('getStore')->willReturn(null); - } - - if ($requestParams) { - $map = []; - - foreach ($requestParams as $name => $value) { - $map[] = [$name, null, $value]; - } - - $this->_request - ->method('getParam') - ->willReturnMap($map); - } - - $expectedDataToEncode = array_merge([ - 'config' => ['show_all_regions' => false, 'regions_required' => []], - ], array_filter($allowedCountries)); - $this->scopeConfigMock->method('getValue') ->willReturnMap( [ @@ -219,47 +120,30 @@ public function testGetRegionJson(?int $currentStoreId, array $allowedCountries, AllowedCountries::ALLOWED_COUNTRIES_PATH, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - 'Country1,Country2,Country3' - ], - [ - AllowedCountries::ALLOWED_COUNTRIES_PATH, - ScopeInterface::SCOPE_WEBSITE, - 1, - 'Country1' - ], - [ - AllowedCountries::ALLOWED_COUNTRIES_PATH, - ScopeInterface::SCOPE_STORE, - 1, - 'Country2' - ], - [ - AllowedCountries::ALLOWED_COUNTRIES_PATH, - ScopeInterface::SCOPE_STORE, - 2, - 'Country3' + 'Country1,Country2' ], [Data::XML_PATH_STATES_REQUIRED, ScopeInterface::SCOPE_STORE, null, ''] ] ); - $regions = [ - new DataObject(['country_id' => 'Country1', 'region_id' => 'r1', 'code' => 'r1-code', 'name' => 'r1-name']), - new DataObject(['country_id' => 'Country1', 'region_id' => 'r2', 'code' => 'r2-code', 'name' => 'r2-name']), - new DataObject(['country_id' => 'Country2', 'region_id' => 'r3', 'code' => 'r3-code', 'name' => 'r3-name']), + new DataObject( + ['country_id' => 'Country1', 'region_id' => 'r1', 'code' => 'r1-code', 'name' => 'r1-name'] + ), + new DataObject( + ['country_id' => 'Country1', 'region_id' => 'r2', 'code' => 'r2-code', 'name' => 'r2-name'] + ), + new DataObject( + ['country_id' => 'Country2', 'region_id' => 'r3', 'code' => 'r3-code', 'name' => 'r3-name'] + ) ]; - $regions = array_filter($regions, function (DataObject $region) use ($allowedCountries) { - return array_key_exists($region->getData('country_id'), $allowedCountries); - }); - - $regionIterator = new ArrayIterator($regions); + $regionIterator = new \ArrayIterator($regions); $this->_regionCollection->expects( $this->once() )->method( 'addCountryFilter' )->with( - array_keys($allowedCountries) + ['Country1', 'Country2'] )->willReturnSelf(); $this->_regionCollection->expects($this->once())->method('load'); $this->_regionCollection->expects( @@ -270,6 +154,14 @@ public function testGetRegionJson(?int $currentStoreId, array $allowedCountries, $regionIterator ); + $expectedDataToEncode = [ + 'config' => ['show_all_regions' => false, 'regions_required' => []], + 'Country1' => [ + 'r1' => ['code' => 'r1-code', 'name' => 'r1-name'], + 'r2' => ['code' => 'r2-code', 'name' => 'r2-name'] + ], + 'Country2' => ['r3' => ['code' => 'r3-code', 'name' => 'r3-name']] + ]; $this->jsonHelperMock->expects( $this->once() )->method( From b611bae3532c7a16255f13f7ad5eabfb518eb838 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" Date: Thu, 21 Jan 2021 15:17:34 +0200 Subject: [PATCH 020/137] added mftf test --- .../Mftf/Data/CountryOptionConfigData.xml | 9 ++++ .../Metadata/SystemConfigCountriesMeta.xml | 14 ++++++ ...ryAllowedOnlyOnCurrentWebsiteScopeTest.xml | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml index 53ca46e746206..378aa0bfc510c 100644 --- a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml +++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml @@ -21,4 +21,13 @@ 0 + + DefaultAdminAccountAllowCountry + + + general/country/allow + US + websites + base + diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/SystemConfigCountriesMeta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/SystemConfigCountriesMeta.xml index bd16c225af51d..100c874024a5e 100644 --- a/app/code/Magento/Config/Test/Mftf/Metadata/SystemConfigCountriesMeta.xml +++ b/app/code/Magento/Config/Test/Mftf/Metadata/SystemConfigCountriesMeta.xml @@ -32,4 +32,18 @@ + + + + + + + + string + + + + + + diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml new file mode 100644 index 0000000000000..7217f452e83f5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + <description value="Place an order when country allowed only on current website scope"/> + <severity value="MAJOR"/> + <group value="customer"/> + </annotations> + <before> + <magentoCLI command="config:set --scope={{SetAllowedCountryUsConfig.scope}} --scope-code={{SetAllowedCountryUsConfig.scope_code}} {{SetAllowedCountryUsConfig.path}} {{SetAllowedCountryUsConfig.value}}" stepKey="setAllowedCountryUs"/> + <magentoCLI command="config:set {{SetAllowedCountryUsConfig.path}} ''" stepKey="unselectAllCountriesFromAllowedCounties"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <createData entity="SetAdminAccountAllowCountryToDefaultForDefaultWebsite" stepKey="setDefaultValueForAllowCountriesForDefaultWebsites"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToTheOrder"> + <argument name="product" value="$createSimpleProduct$"/> + <argument name="productQty" value="2"/> + </actionGroup> + <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder"/> + <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> + </test> +</tests> From e8706158d7f79c720bb3e42a2bec88e5f916e608 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Wed, 27 Jan 2021 16:19:05 -0600 Subject: [PATCH 021/137] MC-38539: Introduce JWT wrapper --- .../Magento/JwtFrameworkAdapter/LICENSE.txt | 48 +++++ .../JwtFrameworkAdapter/LICENSE_AFL.txt | 48 +++++ .../JwtFrameworkAdapter/Model/Data/Claim.php | 73 ++++++++ .../JwtFrameworkAdapter/Model/Data/Header.php | 66 +++++++ .../JwtFrameworkAdapter/Model}/JwtManager.php | 177 +++++++++++++++++- .../Magento/JwtFrameworkAdapter/README.md | 1 + .../Magento/JwtFrameworkAdapter/composer.json | 24 +++ .../Magento/JwtFrameworkAdapter/etc/di.xml | 10 + .../JwtFrameworkAdapter/etc/module.xml | 10 + .../JwtFrameworkAdapter/registration.php | 9 + app/etc/di.xml | 1 - .../Magento/Framework/Jwt/JwtManagerTest.php | 82 +++++++- .../Framework/Jwt/Claim/PrivateClaim.php | 76 ++++++++ .../{ => Header}/PrivateHeaderParameter.php | 2 +- .../Magento/Framework/Jwt/HeaderInterface.php | 8 + lib/internal/Magento/Framework/Jwt/Jwk.php | 32 ++++ .../Magento/Framework/Jwt/JwkFactory.php | 90 +++++++++ .../Magento/Framework/Jwt/Jws/JwsHeader.php | 12 +- .../Jwt/Payload/ArbitraryPayload.php | 36 ++++ .../Framework/Jwt/Payload/ClaimsPayload.php | 48 +++++ .../{ => Payload}/ClaimsPayloadInterface.php | 5 +- .../Framework/Jwt/Payload/NestedPayload.php | 33 ++++ .../{ => Payload}/NestedPayloadInterface.php | 11 +- 23 files changed, 882 insertions(+), 20 deletions(-) create mode 100644 app/code/Magento/JwtFrameworkAdapter/LICENSE.txt create mode 100644 app/code/Magento/JwtFrameworkAdapter/LICENSE_AFL.txt create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php rename {lib/internal/Magento/Framework/Jwt/JwtFrameworkAdapter => app/code/Magento/JwtFrameworkAdapter/Model}/JwtManager.php (50%) create mode 100644 app/code/Magento/JwtFrameworkAdapter/README.md create mode 100644 app/code/Magento/JwtFrameworkAdapter/composer.json create mode 100644 app/code/Magento/JwtFrameworkAdapter/etc/di.xml create mode 100644 app/code/Magento/JwtFrameworkAdapter/etc/module.xml create mode 100644 app/code/Magento/JwtFrameworkAdapter/registration.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php rename lib/internal/Magento/Framework/Jwt/{ => Header}/PrivateHeaderParameter.php (96%) create mode 100644 lib/internal/Magento/Framework/Jwt/JwkFactory.php create mode 100644 lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php create mode 100644 lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php rename lib/internal/Magento/Framework/Jwt/{ => Payload}/ClaimsPayloadInterface.php (72%) create mode 100644 lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php rename lib/internal/Magento/Framework/Jwt/{ => Payload}/NestedPayloadInterface.php (60%) diff --git a/app/code/Magento/JwtFrameworkAdapter/LICENSE.txt b/app/code/Magento/JwtFrameworkAdapter/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/JwtFrameworkAdapter/LICENSE_AFL.txt b/app/code/Magento/JwtFrameworkAdapter/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php new file mode 100644 index 0000000000000..ef3a7d7faff81 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model\Data; + +use Magento\Framework\Jwt\ClaimInterface; + +class Claim implements ClaimInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var mixed + */ + private $value; + + /** + * @var string|null + */ + private $class; + + /** + * @param string $name + * @param mixed $value + * @param string|null $class + */ + public function __construct(string $name, $value, ?string $class) + { + $this->name = $name; + $this->value = $value; + $this->class = $class; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return $this->class; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return false; + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php new file mode 100644 index 0000000000000..9c2c90bb9cd3d --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model\Data; + +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +class Header implements JwsHeaderParameterInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var mixed + */ + private $value; + + /** + * @var int|null + */ + private $class; + + /** + * Header constructor. + * @param string $name + * @param mixed $value + * @param int|null $class + */ + public function __construct(string $name, $value, ?int $class) + { + $this->name = $name; + $this->value = $value; + $this->class = $class; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return $this->class; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/JwtFrameworkAdapter/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php similarity index 50% rename from lib/internal/Magento/Framework/Jwt/JwtFrameworkAdapter/JwtManager.php rename to app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index 74d3ef11ffa05..0e91732083d8b 100644 --- a/lib/internal/Magento/Framework/Jwt/JwtFrameworkAdapter/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -6,10 +6,14 @@ declare(strict_types=1); -namespace Magento\Framework\Jwt\JwtFrameworkAdapter; +namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Core\AlgorithmManagerFactory; use Jose\Component\Signature\JWSBuilder; +use Jose\Component\Signature\JWSVerifierFactory; +use Jose\Component\Signature\Serializer\CompactSerializer; +use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory; use Jose\Easy\AlgorithmProvider; use Magento\Framework\Jwt\EncryptionSettingsInterface; use Magento\Framework\Jwt\Exception\EncryptionException; @@ -18,21 +22,54 @@ use Magento\Framework\Jwt\HeaderInterface; use Magento\Framework\Jwt\Jwe\JweInterface; use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\JwkSet; +use Magento\Framework\Jwt\Jws\Jws; +use Magento\Framework\Jwt\Jws\JwsHeader; use Magento\Framework\Jwt\Jws\JwsInterface; use Magento\Framework\Jwt\Jws\JwsSignatureJwks; +use Magento\Framework\Jwt\Jws\JwsSignatureSettingsInterface; use Magento\Framework\Jwt\JwtInterface; use Magento\Framework\Jwt\JwtManagerInterface; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayload; +use Magento\Framework\Jwt\Payload\NestedPayload; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; use Jose\Component\Core\JWK as AdapterJwk; use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer; use Jose\Component\Signature\Serializer\JSONGeneralSerializer as JwsJsonSerializer; use Jose\Component\Signature\Serializer\JSONFlattenedSerializer as JwsFlatSerializer; +use Jose\Component\Core\JWKSet as AdapterJwkSet; +use Jose\Component\Signature\JWSLoaderFactory; +use Magento\JwtFrameworkAdapter\Model\Data\Claim; +use Magento\JwtFrameworkAdapter\Model\Data\Header; /** * Adapter for jwt-framework. */ class JwtManager implements JwtManagerInterface { + private const JWT_TYPE_JWS = 1; + + private const JWT_TYPE_JWE = 2; + + private const JWT_TYPE_UNSECURED = 3; + + private const JWS_ALGORITHMS = [ + Jwk::ALGORITHM_HS256, + Jwk::ALGORITHM_HS384, + Jwk::ALGORITHM_HS512, + Jwk::ALGORITHM_RS256, + Jwk::ALGORITHM_RS384, + Jwk::ALGORITHM_RS512, + Jwk::ALGORITHM_ES256, + Jwk::ALGORITHM_ES384, + Jwk::ALGORITHM_ES512, + Jwk::ALGORITHM_PS256, + Jwk::ALGORITHM_PS384, + Jwk::ALGORITHM_PS512 + ]; + /** * @var JWSBuilder */ @@ -53,6 +90,11 @@ class JwtManager implements JwtManagerInterface */ private $jwsFlatSerializer; + /** + * @var JWSLoaderFactory + */ + private $jwsLoaderFactory; + /** * JwtManager constructor. */ @@ -73,12 +115,25 @@ public function __construct() \Jose\Component\Signature\Algorithm\ES512::class, \Jose\Component\Signature\Algorithm\EdDSA::class, ]; - $this->jwsBuilder = new JWSBuilder( - new AlgorithmManager((new AlgorithmProvider($jwsAlgorithms))->getAvailableAlgorithms()) - ); + $jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms); + $algorithmManager = new AlgorithmManager($jwsAlgorithmProvider->getAvailableAlgorithms()); + $this->jwsBuilder = new JWSBuilder($algorithmManager); $this->jwsCompactSerializer = new JwsCompactSerializer(); $this->jwsJsonSerializer = new JwsJsonSerializer(); $this->jwsFlatSerializer = new JwsFlatSerializer(); + $jwsSerializerFactory = new JWSSerializerManagerFactory(); + $jwsSerializerFactory->add(new CompactSerializer()); + $jwsSerializerFactory->add(new JwsJsonSerializer()); + $jwsSerializerFactory->add(new JwsFlatSerializer()); + $jwsAlgorithmFactory = new AlgorithmManagerFactory(); + foreach ($jwsAlgorithmProvider->getAvailableAlgorithms() as $algorithm) { + $jwsAlgorithmFactory->add($algorithm->name(), $algorithm); + } + $this->jwsLoaderFactory = new JWSLoaderFactory( + $jwsSerializerFactory, + new JWSVerifierFactory($jwsAlgorithmFactory), + null + ); } /** @@ -99,7 +154,26 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio */ public function read(string $token, array $acceptableEncryption): JwtInterface { - // TODO: Implement read() method. + /** @var JwtInterface|null $read */ + $read = null; + /** @var \Throwable|null $lastException */ + $lastException = null; + foreach ($acceptableEncryption as $encryptionSettings) { + switch ($this->detectJwtType($encryptionSettings)) { + case self::JWT_TYPE_JWS: + try { + $read = $this->readJws($token, $encryptionSettings); + } catch (\Throwable $exception) { + $lastException = $exception; + } + break; + } + } + + if (!$read) { + throw new JwtException('Failed to read JWT', 0, $lastException); + } + return $read; } /** @@ -120,8 +194,17 @@ private function convertToAdapterJwk(Jwk $jwk): AdapterJwk 'x5t' => $jwk->getX509Sha1Thumbprint(), 'x5t#S256' => $jwk->getX509Sha256Thumbprint() ]; + $data = array_merge($data, $jwk->getAlgoData()); + $data = array_filter($data, function ($value) { + return $value !== null; + }); + + return new AdapterJwk($data); + } - return new AdapterJwk(array_merge($data, $jwk->getAlgoData())); + private function convertToAdapterKeySet(JwkSet $jwkSet): AdapterJwkSet + { + return new AdapterJwkSet(array_map([$this, 'convertToAdapterJwk'], $jwkSet->getKeys())); } /** @@ -166,7 +249,7 @@ private function buildJws(JwsInterface $jws, EncryptionSettingsInterface $encryp try { $builder = $this->jwsBuilder->create(); $builder = $builder->withPayload($jws->getPayload()->getContent()); - for ($i = 0; $i <= $signaturesCount; $i++) { + for ($i = 0; $i < $signaturesCount; $i++) { $jwk = $encryptionSettings->getJwkSet()->getKeys()[$i]; $alg = $jwk->getAlgorithm(); if (!$alg) { @@ -200,4 +283,84 @@ private function buildJws(JwsInterface $jws, EncryptionSettingsInterface $encryp throw $exception; } } + + private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): int + { + if ($encryptionSettings instanceof JwsSignatureSettingsInterface) { + return self::JWT_TYPE_JWS; + } + + if ($encryptionSettings->getAlgorithmName() === Jwk::ALGORITHM_NONE) { + return self::JWT_TYPE_UNSECURED; + } + if (in_array($encryptionSettings->getAlgorithmName(), self::JWS_ALGORITHMS, true)) { + return self::JWT_TYPE_JWS; + } + + throw new \RuntimeException('Failed to determine JWT type'); + } + + /** + * Read and verify a JWS token. + * + * @param string $token + * @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings + * @return JwtInterface + */ + private function readJws(string $token, EncryptionSettingsInterface $encryptionSettings): JwtInterface + { + if (!$encryptionSettings instanceof JwsSignatureJwks) { + throw new JwtException('Can only work with JWK settings for JWS tokens'); + } + + $loader = $this->jwsLoaderFactory->create( + ['jws_compact', 'jws_json_flattened', 'jws_json_general'], + array_map( + function (Jwk $jwk) { + return $jwk->getAlgorithm(); + }, + $encryptionSettings->getJwkSet()->getKeys() + ) + ); + $jws = $loader->loadAndVerifyWithKeySet( + $token, $this->convertToAdapterKeySet($encryptionSettings->getJwkSet()), + $signature, + null + ); + if ($signature === null) { + throw new EncryptionException('Failed to verify a JWS token'); + } + $headers = $jws->getSignature($signature); + $protectedHeaders = []; + foreach ($headers->getProtectedHeader() as $header => $headerValue) { + $protectedHeaders[] = new Header($header, $headerValue, null); + } + $publicHeaders = null; + if ($headers->getHeader()) { + $publicHeaders = []; + foreach ($headers->getHeader() as $header => $headerValue) { + $publicHeaders[] = new Header($header, $headerValue, null); + } + } + if ($jws->isPayloadDetached()) { + throw new JwtException('Detached payload is not supported'); + } + $headersMap = array_merge($headers->getHeader(), $headers->getProtectedHeader()); + if (array_key_exists('cty', $headersMap)) { + if ($headersMap['cty'] === NestedPayloadInterface::CONTENT_TYPE) { + $payload = new NestedPayload($jws->getPayload()); + } else { + $payload = new ArbitraryPayload($jws->getPayload()); + } + } else { + $claimData = json_decode($jws->getPayload(), true); + $claims = []; + foreach ($claimData as $name => $value) { + $claims[] = new Claim($name, $value, null); + } + $payload = new ClaimsPayload($claims); + } + + return new Jws([new JwsHeader($protectedHeaders)], $payload, $publicHeaders ? [new JwsHeader($publicHeaders)] : null); + } } diff --git a/app/code/Magento/JwtFrameworkAdapter/README.md b/app/code/Magento/JwtFrameworkAdapter/README.md new file mode 100644 index 0000000000000..4a2f9dc59aef7 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/README.md @@ -0,0 +1 @@ +Provides Magento\Framework\Jwt\JwtManagerInterface implementation based on jwt-framework. diff --git a/app/code/Magento/JwtFrameworkAdapter/composer.json b/app/code/Magento/JwtFrameworkAdapter/composer.json new file mode 100644 index 0000000000000..d90101a746394 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-jwt-framework-adapter", + "description": "JWT Manager implementation based on jwt-framework", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\JwtFrameworkAdapter\\": "" + } + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/etc/di.xml b/app/code/Magento/JwtFrameworkAdapter/etc/di.xml new file mode 100644 index 0000000000000..2a8248c67c9b7 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/etc/di.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\Framework\Jwt\JwtManagerInterface" type="Magento\JwtFrameworkAdapter\Model\JwtManager" /> +</config> diff --git a/app/code/Magento/JwtFrameworkAdapter/etc/module.xml b/app/code/Magento/JwtFrameworkAdapter/etc/module.xml new file mode 100644 index 0000000000000..256d332ef3fec --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_JwtFrameworkAdapter" /> +</config> diff --git a/app/code/Magento/JwtFrameworkAdapter/registration.php b/app/code/Magento/JwtFrameworkAdapter/registration.php new file mode 100644 index 0000000000000..2b21e8fad6520 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_JwtFrameworkAdapter', __DIR__); diff --git a/app/etc/di.xml b/app/etc/di.xml index c7ad3014f067d..b1d81ed70f6b4 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1912,5 +1912,4 @@ </argument> </arguments> </type> - <preference for="Magento\Framework\Jwt\JwtManagerInterface" type="Magento\Framework\Jwt\JwtFrameworkAdapter\JwtManager" /> </config> diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index dca31754b1995..e8a0db9161931 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -8,8 +8,16 @@ namespace Magento\Framework\Jwt; +use Magento\Framework\Jwt\Claim\PrivateClaim; +use Magento\Framework\Jwt\Header\PrivateHeaderParameter; use Magento\Framework\Jwt\Jwe\JweInterface; +use Magento\Framework\Jwt\Jws\Jws; +use Magento\Framework\Jwt\Jws\JwsHeader; use Magento\Framework\Jwt\Jws\JwsInterface; +use Magento\Framework\Jwt\Jws\JwsSignatureJwks; +use Magento\Framework\Jwt\Payload\ClaimsPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -35,12 +43,36 @@ protected function setUp(): void * @param JwtInterface $jwt * @param EncryptionSettingsInterface $encryption * @return void + * + * @dataProvider getTokenVariants */ public function testCreateRead(JwtInterface $jwt, EncryptionSettingsInterface $encryption): void { - $recreated = $this->manager->read($this->manager->create($jwt, $encryption), [$encryption]); + $token = $this->manager->create($jwt, $encryption); + $recreated = $this->manager->read($token, [$encryption]); + + //Verifying header + $this->verifyHeader($jwt->getHeader(), $recreated->getHeader()); + //Verifying payload + $this->assertEquals($jwt->getPayload()->getContent(), $recreated->getPayload()->getContent()); + if ($jwt->getPayload() instanceof ClaimsPayloadInterface) { + $this->assertInstanceOf(ClaimsPayloadInterface::class, $recreated->getPayload()); + } + if ($jwt->getPayload() instanceof NestedPayloadInterface) { + $this->assertInstanceOf(NestedPayloadInterface::class, $recreated->getPayload()); + } + + //JWT type specific validation if ($jwt instanceof JwsInterface) { $this->assertInstanceOf(JwsInterface::class, $recreated); + /** @var JwsInterface $recreated */ + if (!$jwt->getUnprotectedHeaders()) { + $this->assertNull($recreated->getUnprotectedHeaders()); + } else { + $this->assertTrue(count($recreated->getUnprotectedHeaders()) >= 1); + $this->verifyHeader($jwt->getUnprotectedHeaders()[0], $recreated->getUnprotectedHeaders()[0]); + } + $this->verifyHeader($jwt->getProtectedHeaders()[0], $recreated->getProtectedHeaders()[0]); } if ($jwt instanceof JweInterface) { $this->assertInstanceOf(JweInterface::class, $recreated); @@ -48,14 +80,60 @@ public function testCreateRead(JwtInterface $jwt, EncryptionSettingsInterface $e if ($jwt instanceof UnsecuredJwtInterface) { $this->assertInstanceOf(UnsecuredJwtInterface::class, $recreated); } + } public function getTokenVariants(): array { + /** @var JwkFactory $jwkFactory */ + $jwkFactory = Bootstrap::getObjectManager()->get(JwkFactory::class); + + $hmacFlatJws = new Jws( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('custom-header', 'value'), + new PrivateHeaderParameter('another-custom-header', 'value2') + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2'), + new PrivateClaim('custom-claim3', 'value3') + ] + ), + null + ); + return [ 'jws-HS256' => [ - + $hmacFlatJws, + new JwsSignatureJwks($jwkFactory->createHs256(random_bytes(128))) + ], + 'jws-HS384' => [ + $hmacFlatJws, + new JwsSignatureJwks($jwkFactory->createHs384(random_bytes(128))) + ], + 'jws-HS512' => [ + $hmacFlatJws, + new JwsSignatureJwks($jwkFactory->createHs512(random_bytes(128))) ] ]; } + + private function verifyHeader(HeaderInterface $expected, HeaderInterface $actual): void + { + $this->assertTrue( + count($expected->getParameters()) <= count($actual->getParameters()) + ); + foreach ($expected->getParameters() as $parameter) { + $this->assertNotNull($actual->getParameter($parameter->getName())); + $this->assertEquals( + $parameter->getValue(), + $actual->getParameter($parameter->getName())->getValue() + ); + } + } } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php b/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php new file mode 100644 index 0000000000000..90e62689a8866 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * Private non-registered claim. + */ +class PrivateClaim implements ClaimInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var mixed + */ + private $value; + + /** + * @var bool + */ + private $headerDuplicated; + + /** + * @param string $name + * @param mixed $value + * @param bool $headerDuplicated + */ + public function __construct(string $name, $value, bool $headerDuplicated = false) + { + $this->name = $name; + $this->value = $value; + $this->headerDuplicated = $headerDuplicated; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_PRIVATE; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->headerDuplicated; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/PrivateHeaderParameter.php b/lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php similarity index 96% rename from lib/internal/Magento/Framework/Jwt/PrivateHeaderParameter.php rename to lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php index aaca9b889523f..16aef00ddf8ee 100644 --- a/lib/internal/Magento/Framework/Jwt/PrivateHeaderParameter.php +++ b/lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php @@ -6,7 +6,7 @@ declare(strict_types=1); -namespace Magento\Framework\Jwt; +namespace Magento\Framework\Jwt\Header; use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; diff --git a/lib/internal/Magento/Framework/Jwt/HeaderInterface.php b/lib/internal/Magento/Framework/Jwt/HeaderInterface.php index 2474cc1a67b07..44eadd1ab17dc 100644 --- a/lib/internal/Magento/Framework/Jwt/HeaderInterface.php +++ b/lib/internal/Magento/Framework/Jwt/HeaderInterface.php @@ -19,4 +19,12 @@ interface HeaderInterface * @return HeaderParameterInterface[] */ public function getParameters(): array; + + /** + * Find a parameter by name. + * + * @param string $name + * @return HeaderParameterInterface|null + */ + public function getParameter(string $name): ?HeaderParameterInterface; } diff --git a/lib/internal/Magento/Framework/Jwt/Jwk.php b/lib/internal/Magento/Framework/Jwt/Jwk.php index 4a9e50d14d015..0e270a9b42fd4 100644 --- a/lib/internal/Magento/Framework/Jwt/Jwk.php +++ b/lib/internal/Magento/Framework/Jwt/Jwk.php @@ -15,6 +15,12 @@ */ class Jwk { + public const KEY_TYPE_EC = 'EC'; + + public const KEY_TYPE_RSA = 'RSA'; + + public const KEY_TYPE_OCTET = 'oct'; + public const PUBLIC_KEY_USE_SIGNATURE = 'sig'; public const PUBLIC_KEY_USE_ENCRYPTION = 'enc'; @@ -35,6 +41,32 @@ class Jwk public const KEY_OP_DERIVE_BITS = 'deriveBits'; + public const ALGORITHM_NONE = 'none'; + + public const ALGORITHM_HS256 = 'HS256'; + + public const ALGORITHM_HS384 = 'HS384'; + + public const ALGORITHM_HS512 = 'HS512'; + + public const ALGORITHM_RS256 = 'RS256'; + + public const ALGORITHM_RS384 = 'RS384'; + + public const ALGORITHM_RS512 = 'RS512'; + + public const ALGORITHM_ES256 = 'ES256'; + + public const ALGORITHM_ES384 = 'ES384'; + + public const ALGORITHM_ES512 = 'ES512'; + + public const ALGORITHM_PS256 = 'PS256'; + + public const ALGORITHM_PS384 = 'PS384'; + + public const ALGORITHM_PS512 = 'PS512'; + /** * @var string */ diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php new file mode 100644 index 0000000000000..21fffa55441c7 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt; + +/** + * Initiates JWKs for various encryption types. + */ +class JwkFactory +{ + /** + * Create JWK for signatures generated with HMAC and SHA256 + * + * @param string $key + * @return Jwk + */ + public function createHs256(string $key): Jwk + { + return $this->createHmac(256, $key); + } + + /** + * Create JWK for signatures generated with HMAC and SHA384 + * + * @param string $key + * @return Jwk + */ + public function createHs384(string $key): Jwk + { + return $this->createHmac(384, $key); + } + + /** + * Create JWK for signatures generated with HMAC and SHA512 + * + * @param string $key + * @return Jwk + */ + public function createHs512(string $key): Jwk + { + return $this->createHmac(512, $key); + } + + private function createHmac(int $bits, string $key): Jwk + { + if (strlen($key) < 128) { + throw new \InvalidArgumentException('Shared secret key must be at least 128 bits.'); + } + + return new Jwk( + Jwk::KEY_TYPE_OCTET, + ['k' => self::base64Encode($key)], + Jwk::PUBLIC_KEY_USE_SIGNATURE, + null, + 'HS' .$bits + ); + } + + /** + * Encode value into Base64Url format. + * + * @param string $value + * @return string + */ + private static function base64Encode(string $value): string + { + return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); + } + + /** + * Decode Base64Url value. + * + * @param string $encoded + * @return string + */ + private static function base64Decode(string $encoded): string + { + $value = base64_decode(strtr($encoded, '-_', '+/'), true); + if ($value === false) { + throw new \InvalidArgumentException('Invalid base64Url string provided'); + } + + return $value; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php b/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php index ea531cb85381c..a3e37744742cb 100644 --- a/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php +++ b/lib/internal/Magento/Framework/Jwt/Jws/JwsHeader.php @@ -9,6 +9,7 @@ namespace Magento\Framework\Jwt\Jws; use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\HeaderParameterInterface; class JwsHeader implements HeaderInterface { @@ -22,14 +23,15 @@ class JwsHeader implements HeaderInterface */ public function __construct(array $parameters) { + $this->parameters = []; foreach ($parameters as $parameter) { if (!$parameter instanceof JwsHeaderParameterInterface) { throw new \InvalidArgumentException( sprintf('Header "%s" is not applicable to JWS tokens', $parameter->getName()) ); } + $this->parameters[$parameter->getName()] = $parameter; } - $this->parameters = $parameters; } /** @@ -39,4 +41,12 @@ public function getParameters(): array { return $this->parameters; } + + /** + * @inheritDoc + */ + public function getParameter(string $name): ?HeaderParameterInterface + { + return !empty($this->parameters[$name]) ? $this->parameters[$name] : null; + } } diff --git a/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php b/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php new file mode 100644 index 0000000000000..625f7fde73388 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Payload; + +use Magento\Framework\Jwt\PayloadInterface; + +class ArbitraryPayload implements PayloadInterface +{ + /** + * @var string + */ + private $content; + + /** + * ArbitraryPayload constructor. + * @param string $content + */ + public function __construct(string $content) + { + $this->content = $content; + } + + /** + * @inheritDoc + */ + public function getContent(): string + { + return $this->content; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php b/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php new file mode 100644 index 0000000000000..583fd8288cf14 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Payload; + +use Magento\Framework\Jwt\ClaimInterface; + +class ClaimsPayload implements ClaimsPayloadInterface +{ + /** + * @var ClaimInterface[] + */ + private $claims; + + /** + * @param ClaimInterface[] $claims + */ + public function __construct(array $claims) + { + $this->claims = $claims; + } + + /** + * @inheritDoc + */ + public function getClaims(): array + { + return $this->claims; + } + + /** + * @inheritDoc + */ + public function getContent(): string + { + $data = []; + foreach ($this->claims as $claim) { + $data[$claim->getName()] = $claim->getValue(); + } + + return json_encode((object)$data); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/ClaimsPayloadInterface.php b/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayloadInterface.php similarity index 72% rename from lib/internal/Magento/Framework/Jwt/ClaimsPayloadInterface.php rename to lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayloadInterface.php index c7adf882c4adb..b42445ad7d36e 100644 --- a/lib/internal/Magento/Framework/Jwt/ClaimsPayloadInterface.php +++ b/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayloadInterface.php @@ -6,7 +6,10 @@ declare(strict_types=1); -namespace Magento\Framework\Jwt; +namespace Magento\Framework\Jwt\Payload; + +use Magento\Framework\Jwt\ClaimInterface; +use Magento\Framework\Jwt\PayloadInterface; /** * Payload with claims. diff --git a/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php b/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php new file mode 100644 index 0000000000000..86edec5c16eec --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Payload; + +class NestedPayload implements NestedPayloadInterface +{ + /** + * @var string + */ + private $token; + + /** + * @param string $token + */ + public function __construct(string $token) + { + $this->token = $token; + } + + /** + * @inheritDoc + */ + public function getContent(): string + { + return $this->token; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/NestedPayloadInterface.php b/lib/internal/Magento/Framework/Jwt/Payload/NestedPayloadInterface.php similarity index 60% rename from lib/internal/Magento/Framework/Jwt/NestedPayloadInterface.php rename to lib/internal/Magento/Framework/Jwt/Payload/NestedPayloadInterface.php index 4e659b528bf6c..08423576007e9 100644 --- a/lib/internal/Magento/Framework/Jwt/NestedPayloadInterface.php +++ b/lib/internal/Magento/Framework/Jwt/Payload/NestedPayloadInterface.php @@ -6,17 +6,14 @@ declare(strict_types=1); -namespace Magento\Framework\Jwt; +namespace Magento\Framework\Jwt\Payload; + +use Magento\Framework\Jwt\PayloadInterface; /** * Payload with nested JWT. */ interface NestedPayloadInterface extends PayloadInterface { - /** - * JWT Content. - * - * @return JwtInterface - */ - public function getJwt(): JwtInterface; + public const CONTENT_TYPE = 'JWT'; } From cdaca6a0c076b635ced65bf077a3c77786860f96 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Wed, 27 Jan 2021 16:20:38 -0600 Subject: [PATCH 022/137] MC-38539: Introduce JWT wrapper --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a48c1c62e92c8..991db9ba1c915 100644 --- a/composer.json +++ b/composer.json @@ -331,7 +331,8 @@ "magento/module-tinymce-3": "*", "magento/module-csp": "*", "magento/module-aws-s3": "*", - "magento/module-remote-storage": "*" + "magento/module-remote-storage": "*", + "magento/module-jwt-framework-adapter": "*" }, "conflict": { "gene/bluefoot": "*" From 64c2f94afbcc3e4019bfee48e7b6b74e1daefe66 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Fri, 29 Jan 2021 18:58:15 -0600 Subject: [PATCH 023/137] MC-38539: Introduce JWT wrapper --- .../JwtFrameworkAdapter/Model/JwtManager.php | 21 +---- .../Magento/Framework/Jwt/JwtManagerTest.php | 67 +++++++++++++--- .../Framework/Jwt/Header/Algorithm.php | 55 +++++++++++++ .../Magento/Framework/Jwt/Header/Critical.php | 58 ++++++++++++++ .../Magento/Framework/Jwt/Header/Jwk.php | 56 +++++++++++++ .../Framework/Jwt/Header/JwkSetUrl.php | 55 +++++++++++++ .../Magento/Framework/Jwt/Header/KeyId.php | 55 +++++++++++++ .../Jwt/Header/PrivateHeaderParameter.php | 3 +- .../Jwt/Header/PublicHeaderParameter.php | 63 +++++++++++++++ .../Framework/Jwt/Header/X509Chain.php | 58 ++++++++++++++ .../Jwt/Header/X509Sha1Thumbprint.php | 60 ++++++++++++++ .../Jwt/Header/X509Sha256Thumbprint.php | 60 ++++++++++++++ .../Magento/Framework/Jwt/Header/X509Url.php | 55 +++++++++++++ .../Jwt/Jwe/Header/CompressionAlgorithm.php | 54 +++++++++++++ .../Framework/Jwt/Jwe/Header/Encryption.php | 54 +++++++++++++ .../Jwt/Jwe/JweHeaderParameterInterface.php | 17 ++++ lib/internal/Magento/Framework/Jwt/Jwk.php | 24 ++++++ .../Magento/Framework/Jwt/JwkFactory.php | 79 +++++++++++++++++++ .../Jwt/Payload/ArbitraryPayload.php | 18 ++++- .../Framework/Jwt/Payload/ClaimsPayload.php | 10 +++ .../Framework/Jwt/Payload/NestedPayload.php | 8 ++ .../Framework/Jwt/PayloadInterface.php | 7 ++ 22 files changed, 907 insertions(+), 30 deletions(-) create mode 100644 lib/internal/Magento/Framework/Jwt/Header/Algorithm.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/Critical.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/Jwk.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/JwkSetUrl.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/KeyId.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/PublicHeaderParameter.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/X509Chain.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/X509Sha1Thumbprint.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/X509Sha256Thumbprint.php create mode 100644 lib/internal/Magento/Framework/Jwt/Header/X509Url.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/Header/CompressionAlgorithm.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/Header/Encryption.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/JweHeaderParameterInterface.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index 0e91732083d8b..fab4f93bcda54 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -184,22 +184,7 @@ public function read(string $token, array $acceptableEncryption): JwtInterface */ private function convertToAdapterJwk(Jwk $jwk): AdapterJwk { - $data = [ - 'kty' => $jwk->getKeyType(), - 'use' => $jwk->getPublicKeyUse(), - 'key_ops' => $jwk->getKeyOperations(), - 'alg' => $jwk->getAlgorithm(), - 'x5u' => $jwk->getX509Url(), - 'x5c' => $jwk->getX509CertificateChain(), - 'x5t' => $jwk->getX509Sha1Thumbprint(), - 'x5t#S256' => $jwk->getX509Sha256Thumbprint() - ]; - $data = array_merge($data, $jwk->getAlgoData()); - $data = array_filter($data, function ($value) { - return $value !== null; - }); - - return new AdapterJwk($data); + return new AdapterJwk($jwk->getJsonData()); } private function convertToAdapterKeySet(JwkSet $jwkSet): AdapterJwkSet @@ -256,6 +241,9 @@ private function buildJws(JwsInterface $jws, EncryptionSettingsInterface $encryp throw new EncryptionException('Algorithm is required for JWKs'); } $protected = []; + if ($jws->getPayload()->getContentType()) { + $protected['cty'] = $jws->getPayload()->getContentType(); + } if ($jws->getProtectedHeaders()) { $protected = $this->extractHeaderData($jws->getProtectedHeaders()[$i]); } @@ -263,7 +251,6 @@ private function buildJws(JwsInterface $jws, EncryptionSettingsInterface $encryp $unprotected = []; if ($jws->getUnprotectedHeaders()) { $unprotected = $this->extractHeaderData($jws->getUnprotectedHeaders()[$i]); - $unprotected['alg'] = $alg; } $builder = $builder->addSignature($this->convertToAdapterJwk($jwk), $protected, $unprotected); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index e8a0db9161931..1d8d43689ce99 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -9,7 +9,9 @@ namespace Magento\Framework\Jwt; use Magento\Framework\Jwt\Claim\PrivateClaim; +use Magento\Framework\Jwt\Header\Critical; use Magento\Framework\Jwt\Header\PrivateHeaderParameter; +use Magento\Framework\Jwt\Header\PublicHeaderParameter; use Magento\Framework\Jwt\Jwe\JweInterface; use Magento\Framework\Jwt\Jws\Jws; use Magento\Framework\Jwt\Jws\JwsHeader; @@ -42,14 +44,18 @@ protected function setUp(): void * * @param JwtInterface $jwt * @param EncryptionSettingsInterface $encryption + * @param EncryptionSettingsInterface[] $readEncryption * @return void * * @dataProvider getTokenVariants */ - public function testCreateRead(JwtInterface $jwt, EncryptionSettingsInterface $encryption): void - { + public function testCreateRead( + JwtInterface $jwt, + EncryptionSettingsInterface $encryption, + array $readEncryption + ): void { $token = $this->manager->create($jwt, $encryption); - $recreated = $this->manager->read($token, [$encryption]); + $recreated = $this->manager->read($token, $readEncryption); //Verifying header $this->verifyHeader($jwt->getHeader(), $recreated->getHeader()); @@ -88,7 +94,7 @@ public function getTokenVariants(): array /** @var JwkFactory $jwkFactory */ $jwkFactory = Bootstrap::getObjectManager()->get(JwkFactory::class); - $hmacFlatJws = new Jws( + $flatJws = new Jws( [ new JwsHeader( [ @@ -107,18 +113,59 @@ public function getTokenVariants(): array null ); + $flatJwsWithUnprotectedHeader = new Jws( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('custom-header', 'value'), + new Critical(['magento']) + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2') + ] + ), + [ + new JwsHeader( + [ + new PublicHeaderParameter('public-header', 'magento', 'public-value') + ] + ) + ] + ); + $rsaPrivateResource = openssl_pkey_new(['private_key_bites' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); + if ($rsaPrivateResource === false) { + throw new \RuntimeException('Failed to create RSA keypair'); + } + $rsaPublic = openssl_pkey_get_details($rsaPrivateResource)['key']; + if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate)) { + throw new \RuntimeException('Failed to read RSA private key'); + } + openssl_free_key($rsaPrivateResource); + return [ 'jws-HS256' => [ - $hmacFlatJws, - new JwsSignatureJwks($jwkFactory->createHs256(random_bytes(128))) + $flatJws, + $enc = new JwsSignatureJwks($jwkFactory->createHs256(random_bytes(128))), + [$enc] ], 'jws-HS384' => [ - $hmacFlatJws, - new JwsSignatureJwks($jwkFactory->createHs384(random_bytes(128))) + $flatJws, + $enc = new JwsSignatureJwks($jwkFactory->createHs384(random_bytes(128))), + [$enc] ], 'jws-HS512' => [ - $hmacFlatJws, - new JwsSignatureJwks($jwkFactory->createHs512(random_bytes(128))) + $flatJwsWithUnprotectedHeader, + $enc = new JwsSignatureJwks($jwkFactory->createHs512(random_bytes(128))), + [$enc] + ], + 'jws-RS256' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignRs256($rsaPrivate, null)), + [new JwsSignatureJwks($jwkFactory->createVerifyRs256($rsaPublic))] ] ]; } diff --git a/lib/internal/Magento/Framework/Jwt/Header/Algorithm.php b/lib/internal/Magento/Framework/Jwt/Header/Algorithm.php new file mode 100644 index 0000000000000..a3084509946e5 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/Algorithm.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "alg" header. + */ +class Algorithm implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'alg'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/Critical.php b/lib/internal/Magento/Framework/Jwt/Header/Critical.php new file mode 100644 index 0000000000000..7b38463c624bf --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/Critical.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "crit" header. + */ +class Critical implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string[] + */ + private $value; + + /** + * @param string[] $value + */ + public function __construct(array $value) + { + if (!$value) { + throw new \InvalidArgumentException('Critical header cannot be empty'); + } + $this->value = array_values($value); + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'crit'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return json_encode($this->value); + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/Jwk.php b/lib/internal/Magento/Framework/Jwt/Header/Jwk.php new file mode 100644 index 0000000000000..d3aa9a176e38c --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/Jwk.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; +use Magento\Framework\Jwt\Jwk as JwkData; + +/** + * "jwk" header. + */ +class Jwk implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var JwkData + */ + private $value; + + /** + * @param JwkData $value + */ + public function __construct(JwkData $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'jwk'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return json_encode($this->value->getJsonData()); + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/JwkSetUrl.php b/lib/internal/Magento/Framework/Jwt/Header/JwkSetUrl.php new file mode 100644 index 0000000000000..f20bc7e6c1a7c --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/JwkSetUrl.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "jku" header. + */ +class JwkSetUrl implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'jku'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/KeyId.php b/lib/internal/Magento/Framework/Jwt/Header/KeyId.php new file mode 100644 index 0000000000000..7c06477abd973 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/KeyId.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "kid" header. + */ +class KeyId implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'kid'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php b/lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php index 16aef00ddf8ee..1158812838d04 100644 --- a/lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php +++ b/lib/internal/Magento/Framework/Jwt/Header/PrivateHeaderParameter.php @@ -8,9 +8,10 @@ namespace Magento\Framework\Jwt\Header; +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; -class PrivateHeaderParameter implements JwsHeaderParameterInterface +class PrivateHeaderParameter implements JwsHeaderParameterInterface, JweHeaderParameterInterface { /** * @var string diff --git a/lib/internal/Magento/Framework/Jwt/Header/PublicHeaderParameter.php b/lib/internal/Magento/Framework/Jwt/Header/PublicHeaderParameter.php new file mode 100644 index 0000000000000..c6e7ec34d619e --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/PublicHeaderParameter.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * Public header. + */ +class PublicHeaderParameter implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var mixed + */ + private $value; + + /** + * @param string $name + * @param string|null $prefix Prefix for preventing collision. + * @param mixed $value + */ + public function __construct(string $name, ?string $prefix, $value) + { + $this->name = $prefix ? $prefix .'-' .$name : $name; + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_PUBLIC; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/X509Chain.php b/lib/internal/Magento/Framework/Jwt/Header/X509Chain.php new file mode 100644 index 0000000000000..33a4b5c7dfff0 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/X509Chain.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "x5c" header. + */ +class X509Chain implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string[] + */ + private $value; + + /** + * @param string[] $value + */ + public function __construct(array $value) + { + if (count($value) < 1) { + throw new \InvalidArgumentException('X.509 Certificate chain must contain at least 1 key'); + } + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'x5c'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return json_encode(array_map('base64_encode', $this->value)); + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/X509Sha1Thumbprint.php b/lib/internal/Magento/Framework/Jwt/Header/X509Sha1Thumbprint.php new file mode 100644 index 0000000000000..df71409dee6b2 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/X509Sha1Thumbprint.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "x5t" header. + */ +class X509Sha1Thumbprint implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $this->base64UrlEncode($value); + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'x5t'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } + + private function base64UrlEncode(string $key): string + { + return rtrim(strtr(base64_encode($key), '+/', '-_'), '='); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/X509Sha256Thumbprint.php b/lib/internal/Magento/Framework/Jwt/Header/X509Sha256Thumbprint.php new file mode 100644 index 0000000000000..9e4813e3efb03 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/X509Sha256Thumbprint.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "x5t#S256" header. + */ +class X509Sha256Thumbprint implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $this->base64UrlEncode($value); + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'x5t#S256'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } + + private function base64UrlEncode(string $key): string + { + return rtrim(strtr(base64_encode($key), '+/', '-_'), '='); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Header/X509Url.php b/lib/internal/Magento/Framework/Jwt/Header/X509Url.php new file mode 100644 index 0000000000000..ad561dbb8febb --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Header/X509Url.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; +use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; + +/** + * "x5u" header. + */ +class X509Url implements JwsHeaderParameterInterface, JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'x5u'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/Header/CompressionAlgorithm.php b/lib/internal/Magento/Framework/Jwt/Jwe/Header/CompressionAlgorithm.php new file mode 100644 index 0000000000000..d9f2a68ef61af --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/Header/CompressionAlgorithm.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jwe\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; + +/** + * "zip" header. + */ +class CompressionAlgorithm implements JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'zip'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/Header/Encryption.php b/lib/internal/Magento/Framework/Jwt/Jwe/Header/Encryption.php new file mode 100644 index 0000000000000..accff60827f78 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/Header/Encryption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jwe\Header; + +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; + +/** + * "enc" header. + */ +class Encryption implements JweHeaderParameterInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'enc'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return self::CLASS_REGISTERED; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/JweHeaderParameterInterface.php b/lib/internal/Magento/Framework/Jwt/Jwe/JweHeaderParameterInterface.php new file mode 100644 index 0000000000000..8e0fa5f6b74e5 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/JweHeaderParameterInterface.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Jwt\Jwe; + +use Magento\Framework\Jwt\HeaderParameterInterface; + +/** + * Header parameter that is applicable to JWE. + */ +interface JweHeaderParameterInterface extends HeaderParameterInterface +{ + +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwk.php b/lib/internal/Magento/Framework/Jwt/Jwk.php index 0e270a9b42fd4..dd971858ebf45 100644 --- a/lib/internal/Magento/Framework/Jwt/Jwk.php +++ b/lib/internal/Magento/Framework/Jwt/Jwk.php @@ -235,4 +235,28 @@ public function getAlgoData(): array { return $this->data; } + + /** + * JWK data to be represented in JSON. + * + * @return array + */ + public function getJsonData(): array + { + $data = [ + 'kty' => $this->getKeyType(), + 'use' => $this->getPublicKeyUse(), + 'key_ops' => $this->getKeyOperations(), + 'alg' => $this->getAlgorithm(), + 'x5u' => $this->getX509Url(), + 'x5c' => $this->getX509CertificateChain(), + 'x5t' => $this->getX509Sha1Thumbprint(), + 'x5t#S256' => $this->getX509Sha256Thumbprint() + ]; + $data = array_merge($data, $this->getAlgoData()); + + return array_filter($data, function ($value) { + return $value !== null; + }); + } } diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index 21fffa55441c7..d8a33695c5acc 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -46,6 +46,29 @@ public function createHs512(string $key): Jwk return $this->createHmac(512, $key); } + /** + * Create JWK to sign JWS with RSASSA-PKCS1-v1_5 using SHA-256. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignRs256(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignRsa(256, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with RSASSA-PKCS1-v1_5 using SHA-256. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyRs256(string $publicKey): Jwk + { + return $this->createVerifyRsa(256, $publicKey); + } + private function createHmac(int $bits, string $key): Jwk { if (strlen($key) < 128) { @@ -61,6 +84,62 @@ private function createHmac(int $bits, string $key): Jwk ); } + private function createSignRsa(int $bits, string $key, ?string $pass): Jwk + { + $resource = openssl_get_privatekey($key, (string)$pass); + $keyData = openssl_pkey_get_details($resource)['rsa']; + openssl_free_key($resource); + $keysMap = [ + 'n' => 'n', + 'e' => 'e', + 'd' => 'd', + 'p' => 'p', + 'q' => 'q', + 'dp' => 'dmp1', + 'dq' => 'dmq1', + 'qi' => 'iqmp' + ]; + $jwkData = []; + foreach ($keysMap as $jwkKey => $rsaKey) { + if (array_key_exists($rsaKey, $keyData)) { + $jwkData[$jwkKey] = self::base64Encode($keyData[$rsaKey]); + } + } + + return new Jwk( + Jwk::KEY_TYPE_RSA, + $jwkData, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + null, + 'RS' .$bits + ); + } + + private function createVerifyRsa(int $bits, string $key): Jwk + { + $resource = openssl_get_publickey($key); + $keyData = openssl_pkey_get_details($resource)['rsa']; + openssl_free_key($resource); + $keysMap = [ + 'n' => 'n', + 'e' => 'e' + ]; + $jwkData = []; + foreach ($keysMap as $jwkKey => $rsaKey) { + if (array_key_exists($rsaKey, $keyData)) { + $jwkData[$jwkKey] = self::base64Encode($keyData[$rsaKey]); + } + } + + return new Jwk( + Jwk::KEY_TYPE_RSA, + $jwkData, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + null, + 'RS' .$bits + ); + } + /** * Encode value into Base64Url format. * diff --git a/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php b/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php index 625f7fde73388..d5d96607211f4 100644 --- a/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php +++ b/lib/internal/Magento/Framework/Jwt/Payload/ArbitraryPayload.php @@ -18,12 +18,18 @@ class ArbitraryPayload implements PayloadInterface private $content; /** - * ArbitraryPayload constructor. + * @var string|null + */ + private $type; + + /** * @param string $content + * @param string|null $type */ - public function __construct(string $content) + public function __construct(string $content, ?string $type = null) { $this->content = $content; + $this->type = $type; } /** @@ -33,4 +39,12 @@ public function getContent(): string { return $this->content; } + + /** + * @inheritDoc + */ + public function getContentType(): ?string + { + return $this->type; + } } diff --git a/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php b/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php index 583fd8288cf14..0e456008c1632 100644 --- a/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php +++ b/lib/internal/Magento/Framework/Jwt/Payload/ClaimsPayload.php @@ -12,6 +12,8 @@ class ClaimsPayload implements ClaimsPayloadInterface { + public const CONTENT_TYPE = 'json'; + /** * @var ClaimInterface[] */ @@ -45,4 +47,12 @@ public function getContent(): string return json_encode((object)$data); } + + /** + * @inheritDoc + */ + public function getContentType(): ?string + { + return null; + } } diff --git a/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php b/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php index 86edec5c16eec..2a26eecaae124 100644 --- a/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php +++ b/lib/internal/Magento/Framework/Jwt/Payload/NestedPayload.php @@ -30,4 +30,12 @@ public function getContent(): string { return $this->token; } + + /** + * @inheritDoc + */ + public function getContentType(): ?string + { + return self::CONTENT_TYPE; + } } diff --git a/lib/internal/Magento/Framework/Jwt/PayloadInterface.php b/lib/internal/Magento/Framework/Jwt/PayloadInterface.php index 4efaaa5215a74..e481994d9bfa2 100644 --- a/lib/internal/Magento/Framework/Jwt/PayloadInterface.php +++ b/lib/internal/Magento/Framework/Jwt/PayloadInterface.php @@ -19,4 +19,11 @@ interface PayloadInterface * @return string */ public function getContent(): string; + + /** + * Payload type ("cty" header). + * + * @return string|null + */ + public function getContentType(): ?string; } From cde232d7481bc8dde3a89d4546cd7142fa7f2c79 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Fri, 29 Jan 2021 20:05:28 -0600 Subject: [PATCH 024/137] MC-38539: Introduce JWT wrapper --- .../Magento/Framework/Jwt/JwtManagerTest.php | 130 +++++++++++++++--- .../Magento/Framework/Jwt/Claim/Audience.php | 72 ++++++++++ .../Framework/Jwt/Claim/ExpirationTime.php | 73 ++++++++++ .../Magento/Framework/Jwt/Claim/IssuedAt.php | 73 ++++++++++ .../Magento/Framework/Jwt/Claim/Issuer.php | 69 ++++++++++ .../Magento/Framework/Jwt/Claim/JwtId.php | 84 +++++++++++ .../Magento/Framework/Jwt/Claim/NotBefore.php | 73 ++++++++++ .../Framework/Jwt/Claim/PublicClaim.php | 74 ++++++++++ .../Magento/Framework/Jwt/Claim/Subject.php | 69 ++++++++++ .../Magento/Framework/Jwt/JwkFactory.php | 46 +++++++ 10 files changed, 744 insertions(+), 19 deletions(-) create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/Audience.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/Issuer.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/JwtId.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/Subject.php diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 1d8d43689ce99..ff593aba82373 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -8,7 +8,12 @@ namespace Magento\Framework\Jwt; +use Magento\Framework\Jwt\Claim\ExpirationTime; +use Magento\Framework\Jwt\Claim\IssuedAt; +use Magento\Framework\Jwt\Claim\Issuer; +use Magento\Framework\Jwt\Claim\JwtId; use Magento\Framework\Jwt\Claim\PrivateClaim; +use Magento\Framework\Jwt\Claim\Subject; use Magento\Framework\Jwt\Header\Critical; use Magento\Framework\Jwt\Header\PrivateHeaderParameter; use Magento\Framework\Jwt\Header\PublicHeaderParameter; @@ -58,7 +63,9 @@ public function testCreateRead( $recreated = $this->manager->read($token, $readEncryption); //Verifying header - $this->verifyHeader($jwt->getHeader(), $recreated->getHeader()); + if ((!$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) || count($jwt->getProtectedHeaders()) == 1) { + $this->verifyAgainstHeaders([$jwt->getHeader()], $recreated->getHeader()); + } //Verifying payload $this->assertEquals($jwt->getPayload()->getContent(), $recreated->getPayload()->getContent()); if ($jwt->getPayload() instanceof ClaimsPayloadInterface) { @@ -76,9 +83,9 @@ public function testCreateRead( $this->assertNull($recreated->getUnprotectedHeaders()); } else { $this->assertTrue(count($recreated->getUnprotectedHeaders()) >= 1); - $this->verifyHeader($jwt->getUnprotectedHeaders()[0], $recreated->getUnprotectedHeaders()[0]); + $this->verifyAgainstHeaders($jwt->getUnprotectedHeaders(), $recreated->getUnprotectedHeaders()[0]); } - $this->verifyHeader($jwt->getProtectedHeaders()[0], $recreated->getProtectedHeaders()[0]); + $this->verifyAgainstHeaders($jwt->getProtectedHeaders(), $recreated->getProtectedHeaders()[0]); } if ($jwt instanceof JweInterface) { $this->assertInstanceOf(JweInterface::class, $recreated); @@ -107,13 +114,14 @@ public function getTokenVariants(): array [ new PrivateClaim('custom-claim', 'value'), new PrivateClaim('custom-claim2', 'value2'), - new PrivateClaim('custom-claim3', 'value3') + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') ] ), null ); - - $flatJwsWithUnprotectedHeader = new Jws( + $jwsWithUnprotectedHeader = new Jws( [ new JwsHeader( [ @@ -125,7 +133,8 @@ public function getTokenVariants(): array new ClaimsPayload( [ new PrivateClaim('custom-claim', 'value'), - new PrivateClaim('custom-claim2', 'value2') + new PrivateClaim('custom-claim2', 'value2'), + new ExpirationTime(new \DateTimeImmutable()) ] ), [ @@ -136,15 +145,42 @@ public function getTokenVariants(): array ) ] ); + $compactJws = new Jws( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + new JwsHeader( + [ + new PrivateHeaderParameter('test3', true), + new PublicHeaderParameter('test4', 'magento', 'value-another') + ] + ) + ], + new ClaimsPayload([ + new Issuer('magento.com'), + new JwtId(), + new Subject('stuff') + ]), + [ + new JwsHeader([new PrivateHeaderParameter('public', 'header1')]), + new JwsHeader([new PrivateHeaderParameter('public2', 'header')]) + ] + ); + $rsaPrivateResource = openssl_pkey_new(['private_key_bites' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); if ($rsaPrivateResource === false) { throw new \RuntimeException('Failed to create RSA keypair'); } $rsaPublic = openssl_pkey_get_details($rsaPrivateResource)['key']; - if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate)) { + if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate, 'pass')) { throw new \RuntimeException('Failed to read RSA private key'); } openssl_free_key($rsaPrivateResource); + $sharedSecret = random_bytes(128); return [ 'jws-HS256' => [ @@ -158,29 +194,85 @@ public function getTokenVariants(): array [$enc] ], 'jws-HS512' => [ - $flatJwsWithUnprotectedHeader, + $jwsWithUnprotectedHeader, $enc = new JwsSignatureJwks($jwkFactory->createHs512(random_bytes(128))), [$enc] ], 'jws-RS256' => [ $flatJws, - new JwsSignatureJwks($jwkFactory->createSignRs256($rsaPrivate, null)), + new JwsSignatureJwks($jwkFactory->createSignRs256($rsaPrivate, 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyRs256($rsaPublic))] + ], + 'jws-RS384' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignRs384($rsaPrivate, 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyRs384($rsaPublic))] + ], + 'jws-RS512' => [ + $jwsWithUnprotectedHeader, + new JwsSignatureJwks($jwkFactory->createSignRs512($rsaPrivate, 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyRs512($rsaPublic))] + ], + 'jws-compact-multiple-signatures' => [ + $compactJws, + new JwsSignatureJwks( + new JwkSet( + [ + $jwkFactory->createHs384($sharedSecret), + $jwkFactory->createSignRs256($rsaPrivate, 'pass') + ] + ) + ), + [ + new JwsSignatureJwks( + new JwkSet( + [$jwkFactory->createHs384($sharedSecret), $jwkFactory->createVerifyRs256($rsaPublic)] + ) + ) + ] + ], + 'jws-compact-multiple-signatures-one-read' => [ + $compactJws, + new JwsSignatureJwks( + new JwkSet( + [ + $jwkFactory->createHs384($sharedSecret), + $jwkFactory->createSignRs256($rsaPrivate, 'pass') + ] + ) + ), [new JwsSignatureJwks($jwkFactory->createVerifyRs256($rsaPublic))] ] ]; } - private function verifyHeader(HeaderInterface $expected, HeaderInterface $actual): void + private function validateHeader(HeaderInterface $expected, HeaderInterface $actual): void { - $this->assertTrue( - count($expected->getParameters()) <= count($actual->getParameters()) - ); + if (count($expected->getParameters()) > count($actual->getParameters())) { + throw new \InvalidArgumentException('Missing header parameters'); + } foreach ($expected->getParameters() as $parameter) { - $this->assertNotNull($actual->getParameter($parameter->getName())); - $this->assertEquals( - $parameter->getValue(), - $actual->getParameter($parameter->getName())->getValue() - ); + if ($actual->getParameter($parameter->getName()) === null) { + throw new \InvalidArgumentException('Missing header parameters'); + } + if ($actual->getParameter($parameter->getName())->getValue() !== $parameter->getValue()) { + throw new \InvalidArgumentException('Invalid header data'); + } + } + } + + private function verifyAgainstHeaders(array $expected, HeaderInterface $actual): void + { + $oneIsValid = false; + foreach ($expected as $item) { + try { + $this->validateHeader($item, $actual); + $oneIsValid = true; + break; + } catch (\InvalidArgumentException $ex) { + $oneIsValid = false; + } } + $this->assertTrue($oneIsValid); } } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/Audience.php b/lib/internal/Magento/Framework/Jwt/Claim/Audience.php new file mode 100644 index 0000000000000..3bfce6f29a9f2 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/Audience.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "aud" claim. + */ +class Audience implements ClaimInterface +{ + /** + * @var string[] + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param string[] $value + * @param bool $duplicate + */ + public function __construct(array $value, bool $duplicate = false) + { + if (!$value) { + throw new \InvalidArgumentException("Audience list cannot be empty"); + } + $this->value = $value; + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'aud'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return json_encode($this->value); + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php b/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php new file mode 100644 index 0000000000000..bc02019ec7462 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "exp" claim. + */ +class ExpirationTime implements ClaimInterface +{ + /** + * @var string + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param \DateTimeInterface $value + * @param bool $duplicate + */ + public function __construct(\DateTimeInterface $value, bool $duplicate = false) + { + if ($value instanceof \DateTimeImmutable) { + $value = \DateTime::createFromImmutable($value); + } + $value->setTimezone(new \DateTimeZone('UTC')); + $this->value = $value->format('Y-m-d\TH:i:s\Z UTC'); + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'exp'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php b/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php new file mode 100644 index 0000000000000..9dafcabba7738 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "iat" claim. + */ +class IssuedAt implements ClaimInterface +{ + /** + * @var string + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param \DateTimeInterface $value + * @param bool $duplicate + */ + public function __construct(\DateTimeInterface $value, bool $duplicate = false) + { + if ($value instanceof \DateTimeImmutable) { + $value = \DateTime::createFromImmutable($value); + } + $value->setTimezone(new \DateTimeZone('UTC')); + $this->value = $value->format('Y-m-d\TH:i:s\Z UTC'); + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'iat'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php b/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php new file mode 100644 index 0000000000000..c78b5294019e8 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "iss" claim. + */ +class Issuer implements ClaimInterface +{ + /** + * @var string + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param string $value + * @param bool $duplicate + */ + public function __construct(string $value, bool $duplicate = false) + { + $this->value = $value; + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'iss'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php b/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php new file mode 100644 index 0000000000000..8964a1f7f35f4 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "jti" claim. + */ +class JwtId implements ClaimInterface +{ + /** + * @var string + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param string|null $value + * @param bool $duplicate + */ + public function __construct(?string $value = null, bool $duplicate = false) + { + $this->value = $value ?? $this->generateRandom(); + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'jti'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } + + private function generateRandom(): string + { + return implode('', array_map( + function($value) { + return chr($value); + }, + array_map( + function() { + return random_int(33, 126); + }, + array_fill(0, 21, null) + ) + )); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php b/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php new file mode 100644 index 0000000000000..53dff7045229b --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "nbf" claim. + */ +class NotBefore implements ClaimInterface +{ + /** + * @var string + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param \DateTimeInterface $value + * @param bool $duplicate + */ + public function __construct(\DateTimeInterface $value, bool $duplicate = false) + { + if ($value instanceof \DateTimeImmutable) { + $value = \DateTime::createFromImmutable($value); + } + $value->setTimezone(new \DateTimeZone('UTC')); + $this->value = $value->format('Y-m-d\TH:i:s\Z UTC'); + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'nbf'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php b/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php new file mode 100644 index 0000000000000..35e0a4a21320a --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +class PublicClaim implements ClaimInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var mixed + */ + private $value; + + /** + * @var bool + */ + private $headerDuplicated; + + /** + * @param string $name + * @param mixed $value + * @param string|null $prefix + * @param bool $headerDuplicated + */ + public function __construct(string $name, $value, ?string $prefix, bool $headerDuplicated = false) + { + $this->name = $prefix ? $prefix .'-' .$name : $name; + $this->value = $value; + $this->headerDuplicated = $headerDuplicated; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_PUBLIC; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->headerDuplicated; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/Subject.php b/lib/internal/Magento/Framework/Jwt/Claim/Subject.php new file mode 100644 index 0000000000000..b6c479d6fbb23 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/Subject.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * "sub" claim. + */ +class Subject implements ClaimInterface +{ + /** + * @var string + */ + private $value; + + /** + * @var bool + */ + private $duplicate; + + /** + * @param string $value + * @param bool $duplicate + */ + public function __construct(string $value, bool $duplicate = false) + { + $this->value = $value; + $this->duplicate = $duplicate; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'sub'; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?string + { + return self::CLASS_REGISTERED; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicate; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index d8a33695c5acc..dde423ce41118 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -69,6 +69,52 @@ public function createVerifyRs256(string $publicKey): Jwk return $this->createVerifyRsa(256, $publicKey); } + /** + * Create JWK to sign JWS with RSASSA-PKCS1-v1_5 using SHA-384. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignRs384(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignRsa(384, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with RSASSA-PKCS1-v1_5 using SHA-384. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyRs384(string $publicKey): Jwk + { + return $this->createVerifyRsa(384, $publicKey); + } + + /** + * Create JWK to sign JWS with RSASSA-PKCS1-v1_5 using SHA-512. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignRs512(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignRsa(512, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with RSASSA-PKCS1-v1_5 using SHA-512. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyRs512(string $publicKey): Jwk + { + return $this->createVerifyRsa(512, $publicKey); + } + private function createHmac(int $bits, string $key): Jwk { if (strlen($key) < 128) { From cbe1cd939ab81f17054974380f83989f44311db8 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Tue, 2 Feb 2021 15:16:53 +0200 Subject: [PATCH 025/137] replaced value by constant --- app/code/Magento/Directory/Helper/Data.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 2e89d2ecd7e13..b2fd3971ecc8b 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -13,6 +13,7 @@ use Magento\Directory\Model\ResourceModel\Region\CollectionFactory; use Magento\Framework\App\Cache\Type\Config; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; use Magento\Framework\Json\Helper\Data as JsonData; use Magento\Store\Model\ScopeInterface; @@ -25,8 +26,10 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Data extends \Magento\Framework\App\Helper\AbstractHelper +class Data extends AbstractHelper { + private const STORE_ID = 'store_id'; + /** * Config value that lists ISO2 country codes which have optional Zip/Postal pre-configured */ @@ -419,10 +422,10 @@ private function getCurrentScope(): array 'type' => ScopeInterface::SCOPE_WEBSITE, 'value' => $request->getParam(ScopeInterface::SCOPE_WEBSITE), ]; - } elseif ($request->getParam('store_id')) { + } elseif ($request->getParam(self::STORE_ID)) { $scope = [ 'type' => ScopeInterface::SCOPE_STORE, - 'value' => $request->getParam('store_id'), + 'value' => $request->getParam(self::STORE_ID), ]; } From cfb09d224b823f1b45566efee1407c679ef892a2 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 2 Feb 2021 12:17:56 -0600 Subject: [PATCH 026/137] MC-38539: Introduce JWT wrapper --- .../Magento/Framework/Jwt/JwtManagerTest.php | 41 +++++- .../Magento/Framework/Jwt/JwkFactory.php | 126 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index ff593aba82373..5d7dc69f97228 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -171,6 +171,7 @@ public function getTokenVariants(): array ] ); + //RSA keys $rsaPrivateResource = openssl_pkey_new(['private_key_bites' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); if ($rsaPrivateResource === false) { throw new \RuntimeException('Failed to create RSA keypair'); @@ -180,6 +181,29 @@ public function getTokenVariants(): array throw new \RuntimeException('Failed to read RSA private key'); } openssl_free_key($rsaPrivateResource); + + //EC Keys + $curveNameMap = [ + 256 => 'prime256v1', + 384 => 'secp384r1', + 512 => 'secp521r1' + ]; + $ecKeys = []; + foreach ($curveNameMap as $bits => $curve) { + $privateResource = openssl_pkey_new(['curve_name' => $curve, 'private_key_type' => OPENSSL_KEYTYPE_EC]); + if ($privateResource === false) { + throw new \RuntimeException('Failed to create EC keypair'); + } + $esPublic = openssl_pkey_get_details($privateResource)['key']; + if (!openssl_pkey_export($privateResource, $esPrivate, 'pass')) { + throw new \RuntimeException('Failed to read EC private key'); + } + openssl_free_key($privateResource); + $ecKeys[$bits] = [$esPrivate, $esPublic]; + unset($privateResource, $esPublic, $esPrivate); + } + + //Shared secret for SHA algorithms $sharedSecret = random_bytes(128); return [ @@ -242,7 +266,22 @@ public function getTokenVariants(): array ) ), [new JwsSignatureJwks($jwkFactory->createVerifyRs256($rsaPublic))] - ] + ], + 'jws-ES256' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignEs256($ecKeys[256][0], 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyEs256($ecKeys[256][1]))] + ], + 'jws-ES384' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignEs384($ecKeys[384][0], 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyEs384($ecKeys[384][1]))] + ], + 'jws-ES512' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignEs512($ecKeys[512][0], 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyEs512($ecKeys[512][1]))] + ], ]; } diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index dde423ce41118..f82287f1c4488 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -13,6 +13,12 @@ */ class JwkFactory { + private const EC_CURVE_MAP = [ + '1.2.840.10045.3.1.7' => ['name' => 'P-256', 'bits' => 256], + '1.3.132.0.34' => ['name' => 'P-384', 'bits' => 384], + '1.3.132.0.35' => ['name' => 'P-521', 'bits' => 512] + ]; + /** * Create JWK for signatures generated with HMAC and SHA256 * @@ -115,6 +121,75 @@ public function createVerifyRs512(string $publicKey): Jwk return $this->createVerifyRsa(512, $publicKey); } + /** + * Create JWK to sign JWS with ECDSA using P-256 and SHA-256. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignEs256(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignEs(256, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with ECDSA using P-256 and SHA-256. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyEs256(string $publicKey): Jwk + { + return $this->createVerifyEs(256, $publicKey); + } + + /** + * Create JWK to sign JWS with ECDSA using P-384 and SHA-384 . + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignEs384(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignEs(384, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with ECDSA using P-384 and SHA-384 . + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyEs384(string $publicKey): Jwk + { + return $this->createVerifyEs(384, $publicKey); + } + + /** + * Create JWK to sign JWS with ECDSA using P-521 and SHA-512. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignEs512(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignEs(512, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with ECDSA using P-521 and SHA-512. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyEs512(string $publicKey): Jwk + { + return $this->createVerifyEs(512, $publicKey); + } + private function createHmac(int $bits, string $key): Jwk { if (strlen($key) < 128) { @@ -186,6 +261,57 @@ private function createVerifyRsa(int $bits, string $key): Jwk ); } + private function createSignEs(int $bits, string $key, ?string $pass): Jwk + { + $resource = openssl_get_privatekey($key, (string)$pass); + $keyData = openssl_pkey_get_details($resource)['ec']; + openssl_free_key($resource); + if (!array_key_exists($keyData['curve_oid'], self::EC_CURVE_MAP)) { + throw new \RuntimeException('Unsupported EC curve'); + } + if ($bits !== self::EC_CURVE_MAP[$keyData['curve_oid']]['bits']) { + throw new \RuntimeException('The key cannot be used with SHA-' .$bits .' hashing algorithm'); + } + + return new Jwk( + Jwk::KEY_TYPE_EC, + [ + 'd' => self::base64Encode($keyData['d']), + 'x' => self::base64Encode($keyData['x']), + 'y' => self::base64Encode($keyData['y']), + 'crv' => self::EC_CURVE_MAP[$keyData['curve_oid']]['name'] + ], + Jwk::PUBLIC_KEY_USE_SIGNATURE, + null, + 'ES' .$bits + ); + } + + private function createVerifyEs(int $bits, string $key): Jwk + { + $resource = openssl_get_publickey($key); + $keyData = openssl_pkey_get_details($resource)['ec']; + openssl_free_key($resource); + if (!array_key_exists($keyData['curve_oid'], self::EC_CURVE_MAP)) { + throw new \RuntimeException('Unsupported EC curve'); + } + if ($bits !== self::EC_CURVE_MAP[$keyData['curve_oid']]['bits']) { + throw new \RuntimeException('The key cannot be used with SHA-' .$bits .' hashing algorithm'); + } + + return new Jwk( + Jwk::KEY_TYPE_EC, + [ + 'x' => self::base64Encode($keyData['x']), + 'y' => self::base64Encode($keyData['y']), + 'crv' => self::EC_CURVE_MAP[$keyData['curve_oid']]['name'] + ], + Jwk::PUBLIC_KEY_USE_SIGNATURE, + null, + 'ES' .$bits + ); + } + /** * Encode value into Base64Url format. * From d198c2e196c7d2c81562fc03f533ed480c29e9a7 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 2 Feb 2021 13:09:31 -0600 Subject: [PATCH 027/137] MC-38539: Introduce JWT wrapper --- .../Magento/Framework/Jwt/JwtManagerTest.php | 107 ++++++++++----- lib/internal/Magento/Framework/Jwt/Jwk.php | 23 +++- .../Magento/Framework/Jwt/JwkFactory.php | 129 ++++++++++++++++++ 3 files changed, 221 insertions(+), 38 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 5d7dc69f97228..339ef21121834 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -171,55 +171,25 @@ public function getTokenVariants(): array ] ); - //RSA keys - $rsaPrivateResource = openssl_pkey_new(['private_key_bites' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); - if ($rsaPrivateResource === false) { - throw new \RuntimeException('Failed to create RSA keypair'); - } - $rsaPublic = openssl_pkey_get_details($rsaPrivateResource)['key']; - if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate, 'pass')) { - throw new \RuntimeException('Failed to read RSA private key'); - } - openssl_free_key($rsaPrivateResource); - - //EC Keys - $curveNameMap = [ - 256 => 'prime256v1', - 384 => 'secp384r1', - 512 => 'secp521r1' - ]; - $ecKeys = []; - foreach ($curveNameMap as $bits => $curve) { - $privateResource = openssl_pkey_new(['curve_name' => $curve, 'private_key_type' => OPENSSL_KEYTYPE_EC]); - if ($privateResource === false) { - throw new \RuntimeException('Failed to create EC keypair'); - } - $esPublic = openssl_pkey_get_details($privateResource)['key']; - if (!openssl_pkey_export($privateResource, $esPrivate, 'pass')) { - throw new \RuntimeException('Failed to read EC private key'); - } - openssl_free_key($privateResource); - $ecKeys[$bits] = [$esPrivate, $esPublic]; - unset($privateResource, $esPublic, $esPrivate); - } - - //Shared secret for SHA algorithms + //Keys + [$rsaPrivate, $rsaPublic] = $this->createRsaKeys(); + $ecKeys = $this->createEcKeys(); $sharedSecret = random_bytes(128); return [ 'jws-HS256' => [ $flatJws, - $enc = new JwsSignatureJwks($jwkFactory->createHs256(random_bytes(128))), + $enc = new JwsSignatureJwks($jwkFactory->createHs256($sharedSecret)), [$enc] ], 'jws-HS384' => [ $flatJws, - $enc = new JwsSignatureJwks($jwkFactory->createHs384(random_bytes(128))), + $enc = new JwsSignatureJwks($jwkFactory->createHs384($sharedSecret)), [$enc] ], 'jws-HS512' => [ $jwsWithUnprotectedHeader, - $enc = new JwsSignatureJwks($jwkFactory->createHs512(random_bytes(128))), + $enc = new JwsSignatureJwks($jwkFactory->createHs512($sharedSecret)), [$enc] ], 'jws-RS256' => [ @@ -282,6 +252,21 @@ public function getTokenVariants(): array new JwsSignatureJwks($jwkFactory->createSignEs512($ecKeys[512][0], 'pass')), [new JwsSignatureJwks($jwkFactory->createVerifyEs512($ecKeys[512][1]))] ], + 'jws-PS256' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignPs256($rsaPrivate, 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyPs256($rsaPublic))] + ], + 'jws-PS384' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignPs384($rsaPrivate, 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyPs384($rsaPublic))] + ], + 'jws-PS512' => [ + $flatJws, + new JwsSignatureJwks($jwkFactory->createSignPs512($rsaPrivate, 'pass')), + [new JwsSignatureJwks($jwkFactory->createVerifyPs512($rsaPublic))] + ], ]; } @@ -314,4 +299,54 @@ private function verifyAgainstHeaders(array $expected, HeaderInterface $actual): } $this->assertTrue($oneIsValid); } + + /** + * Create RSA key-pair. + * + * @return string[] With 1st element as private key, second - public. + */ + private function createRsaKeys(): array + { + $rsaPrivateResource = openssl_pkey_new(['private_key_bites' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); + if ($rsaPrivateResource === false) { + throw new \RuntimeException('Failed to create RSA keypair'); + } + $rsaPublic = openssl_pkey_get_details($rsaPrivateResource)['key']; + if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate, 'pass')) { + throw new \RuntimeException('Failed to read RSA private key'); + } + openssl_free_key($rsaPrivateResource); + + return [$rsaPrivate, $rsaPublic]; + } + + /** + * Create EC key pairs for with different curves. + * + * @return array Keys - bits, values contain 2 elements: 0 => private, 1 => public. + */ + private function createEcKeys(): array + { + $curveNameMap = [ + 256 => 'prime256v1', + 384 => 'secp384r1', + 512 => 'secp521r1' + ]; + $ecKeys = []; + foreach ($curveNameMap as $bits => $curve) { + $privateResource = openssl_pkey_new(['curve_name' => $curve, 'private_key_type' => OPENSSL_KEYTYPE_EC]); + if ($privateResource === false) { + throw new \RuntimeException('Failed to create EC keypair'); + } + $esPublic = openssl_pkey_get_details($privateResource)['key']; + if (!openssl_pkey_export($privateResource, $esPrivate, 'pass')) { + throw new \RuntimeException('Failed to read EC private key'); + } + openssl_free_key($privateResource); + $ecKeys[$bits] = [$esPrivate, $esPublic]; + unset($privateResource, $esPublic, $esPrivate); + } + + return $ecKeys; + } } diff --git a/lib/internal/Magento/Framework/Jwt/Jwk.php b/lib/internal/Magento/Framework/Jwt/Jwk.php index dd971858ebf45..692413f9a4993 100644 --- a/lib/internal/Magento/Framework/Jwt/Jwk.php +++ b/lib/internal/Magento/Framework/Jwt/Jwk.php @@ -77,6 +77,11 @@ class Jwk */ private $use; + /** + * @var string|null + */ + private $kid; + /** * @var string[]|null */ @@ -123,6 +128,7 @@ class Jwk * @param string[]|null $x5c * @param string|null $x5t * @param string|null $x5ts256 + * @param string|null $kid */ public function __construct( string $kty, @@ -133,7 +139,8 @@ public function __construct( ?string $x5u = null, ?array $x5c = null, ?string $x5t = null, - ?string $x5ts256 = null + ?string $x5ts256 = null, + ?string $kid = null ) { $this->kty = $kty; $this->data = $data; @@ -144,6 +151,7 @@ public function __construct( $this->x5c = $x5c; $this->x5t = $x5t; $this->x5ts256 = $x5ts256; + $this->kid = $kid; } /** @@ -226,6 +234,16 @@ public function getX509Sha256Thumbprint(): ?string return $this->x5ts256; } + /** + * "kid" parameter. + * + * @return string|null + */ + public function getKeyId(): ?string + { + return $this->kid; + } + /** * Map with algorithm (type) specific data. * @@ -251,7 +269,8 @@ public function getJsonData(): array 'x5u' => $this->getX509Url(), 'x5c' => $this->getX509CertificateChain(), 'x5t' => $this->getX509Sha1Thumbprint(), - 'x5t#S256' => $this->getX509Sha256Thumbprint() + 'x5t#S256' => $this->getX509Sha256Thumbprint(), + 'kid' => $this->getKeyId() ]; $data = array_merge($data, $this->getAlgoData()); diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index f82287f1c4488..ba02a9d61472d 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -19,6 +19,50 @@ class JwkFactory '1.3.132.0.35' => ['name' => 'P-521', 'bits' => 512] ]; + /** + * Create JWK object from key data. + * + * @param array $data + * @return Jwk + */ + public function createFromData(array $data): Jwk + { + if (!array_key_exists('kty', $data)) { + throw new \InvalidArgumentException('Missing key type in JWK data (kty)'); + } + $kty = $data['kty']; + unset($data['kty']); + $use = array_key_exists('use', $data) ? $data['use'] : null; + unset($data['use']); + $keyOps = array_key_exists('key_ops', $data) ? $data['key_ops'] : null; + unset($data['key_ops']); + $alg = array_key_exists('alg', $data) ? $data['alg'] : null; + unset($data['alg']); + $x5u = array_key_exists('x5u', $data) ? $data['x5u'] : null; + unset($data['use']); + $x5c = array_key_exists('x5c', $data) ? $data['x5c'] : null; + unset($data['x5c']); + $x5t = array_key_exists('x5t', $data) ? $data['x5t'] : null; + unset($data['x5t']); + $x5tS256 = array_key_exists('x5t#S256', $data) ? $data['x5t#S256'] : null; + unset($data['x5t#S256']); + $kid = array_key_exists('kid', $data) ? $data['kid'] : null; + unset($data['kid']); + + return new Jwk( + $kty, + $data, + $use, + $keyOps, + $alg, + $x5u, + $x5c, + $x5t, + $x5tS256, + $kid + ); + } + /** * Create JWK for signatures generated with HMAC and SHA256 * @@ -190,6 +234,75 @@ public function createVerifyEs512(string $publicKey): Jwk return $this->createVerifyEs(512, $publicKey); } + /** + * Create JWK to sign JWS with RSASSA-PSS using SHA-256 and MGF1 with SHA-256. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignPs256(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignPs(256, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with RSASSA-PSS using SHA-256 and MGF1 with SHA-256. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyPs256(string $publicKey): Jwk + { + return $this->createVerifyPs(256, $publicKey); + } + + /** + * Create JWK to sign JWS with RSASSA-PSS using SHA-384 and MGF1 with SHA-384. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignPs384(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignPs(384, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with RSASSA-PSS using SHA-384 and MGF1 with SHA-384. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyPs384(string $publicKey): Jwk + { + return $this->createVerifyPs(384, $publicKey); + } + + /** + * Create JWK to sign JWS with RSASSA-PSS using SHA-512 and MGF1 with SHA-512. + * + * @param string $privateKey + * @param string|null $passPhrase + * @return Jwk + */ + public function createSignPs512(string $privateKey, ?string $passPhrase): Jwk + { + return $this->createSignPs(512, $privateKey, $passPhrase); + } + + /** + * Create JWK to verify JWS signed with RSASSA-PSS using SHA-512 and MGF1 with SHA-512. + * + * @param string $publicKey + * @return Jwk + */ + public function createVerifyPs512(string $publicKey): Jwk + { + return $this->createVerifyPs(512, $publicKey); + } + private function createHmac(int $bits, string $key): Jwk { if (strlen($key) < 128) { @@ -261,6 +374,22 @@ private function createVerifyRsa(int $bits, string $key): Jwk ); } + private function createSignPs(int $bits, string $key, ?string $pass): Jwk + { + $data = $this->createSignRsa($bits, $key, $pass)->getJsonData(); + $data['alg'] = 'PS' .$bits; + + return $this->createFromData($data); + } + + private function createVerifyPs(int $bits, string $key): Jwk + { + $data = $this->createVerifyRsa($bits, $key)->getJsonData(); + $data['alg'] = 'PS' .$bits; + + return $this->createFromData($data); + } + private function createSignEs(int $bits, string $key, ?string $pass): Jwk { $resource = openssl_get_privatekey($key, (string)$pass); From 6ee7f3b83724d2b4c506ff40891b701d97e544b5 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 2 Feb 2021 14:13:56 -0600 Subject: [PATCH 028/137] MC-38539: Introduce JWT wrapper --- .../Model/JwsBuilderFactory.php | 39 +++ .../JwtFrameworkAdapter/Model/JwsFactory.php | 64 +++++ .../Model/JwsLoaderFactory.php | 54 ++++ .../JwtFrameworkAdapter/Model/JwsManager.php | 194 +++++++++++++ .../Model/JwsSerializerPoolFactory.php | 27 ++ .../JwtFrameworkAdapter/Model/JwtManager.php | 265 ++---------------- 6 files changed, 397 insertions(+), 246 deletions(-) create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JwsFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php new file mode 100644 index 0000000000000..cb9801defd19f --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Signature\JWSBuilder; +use Jose\Easy\AlgorithmProvider; + +class JwsBuilderFactory +{ + public function create(): JWSBuilder + { + $jwsAlgorithms = [ + \Jose\Component\Signature\Algorithm\HS256::class, + \Jose\Component\Signature\Algorithm\HS384::class, + \Jose\Component\Signature\Algorithm\HS512::class, + \Jose\Component\Signature\Algorithm\RS256::class, + \Jose\Component\Signature\Algorithm\RS384::class, + \Jose\Component\Signature\Algorithm\RS512::class, + \Jose\Component\Signature\Algorithm\PS256::class, + \Jose\Component\Signature\Algorithm\PS384::class, + \Jose\Component\Signature\Algorithm\PS512::class, + \Jose\Component\Signature\Algorithm\ES256::class, + \Jose\Component\Signature\Algorithm\ES384::class, + \Jose\Component\Signature\Algorithm\ES512::class, + \Jose\Component\Signature\Algorithm\EdDSA::class, + ]; + $jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms); + $algorithmManager = new AlgorithmManager($jwsAlgorithmProvider->getAvailableAlgorithms()); + + return new JWSBuilder($algorithmManager); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsFactory.php new file mode 100644 index 0000000000000..7a5aa282eed5c --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsFactory.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Magento\Framework\Jwt\Jws\Jws; +use Magento\Framework\Jwt\Jws\JwsHeader; +use Magento\Framework\Jwt\Jws\JwsInterface; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayload; +use Magento\Framework\Jwt\Payload\NestedPayload; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\JwtFrameworkAdapter\Model\Data\Claim; +use Magento\JwtFrameworkAdapter\Model\Data\Header; + +/** + * Create JWS data object. + */ +class JwsFactory +{ + public function create( + array $protectedHeadersMap, + string $payload, + ?array $unprotectedHeadersMap + ): JwsInterface { + $protectedHeaders = []; + foreach ($protectedHeadersMap as $header => $headerValue) { + $protectedHeaders[] = new Header($header, $headerValue, null); + } + $publicHeaders = null; + if ($unprotectedHeadersMap) { + $publicHeaders = []; + foreach ($unprotectedHeadersMap as $header => $headerValue) { + $publicHeaders[] = new Header($header, $headerValue, null); + } + } + $headersMap = array_merge($unprotectedHeadersMap ?? [], $protectedHeadersMap); + if (array_key_exists('cty', $headersMap)) { + if ($headersMap['cty'] === NestedPayloadInterface::CONTENT_TYPE) { + $payload = new NestedPayload($payload); + } else { + $payload = new ArbitraryPayload($payload); + } + } else { + $claimData = json_decode($payload, true); + $claims = []; + foreach ($claimData as $name => $value) { + $claims[] = new Claim($name, $value, null); + } + $payload = new ClaimsPayload($claims); + } + + return new Jws( + [new JwsHeader($protectedHeaders)], + $payload, + $publicHeaders ? [new JwsHeader($publicHeaders)] : null + ); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php new file mode 100644 index 0000000000000..aba88f89207b0 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManagerFactory; +use Jose\Component\Signature\JWSVerifierFactory; +use Jose\Component\Signature\Serializer\CompactSerializer; +use Jose\Component\Signature\Serializer\JSONFlattenedSerializer as JwsFlatSerializer; +use Jose\Component\Signature\Serializer\JSONGeneralSerializer as JwsJsonSerializer; +use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory; +use Jose\Easy\AlgorithmProvider; + +class JwsLoaderFactory +{ + public function create() + { + $jwsAlgorithms = [ + \Jose\Component\Signature\Algorithm\HS256::class, + \Jose\Component\Signature\Algorithm\HS384::class, + \Jose\Component\Signature\Algorithm\HS512::class, + \Jose\Component\Signature\Algorithm\RS256::class, + \Jose\Component\Signature\Algorithm\RS384::class, + \Jose\Component\Signature\Algorithm\RS512::class, + \Jose\Component\Signature\Algorithm\PS256::class, + \Jose\Component\Signature\Algorithm\PS384::class, + \Jose\Component\Signature\Algorithm\PS512::class, + \Jose\Component\Signature\Algorithm\ES256::class, + \Jose\Component\Signature\Algorithm\ES384::class, + \Jose\Component\Signature\Algorithm\ES512::class, + \Jose\Component\Signature\Algorithm\EdDSA::class, + ]; + $jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms); + $jwsAlgorithmFactory = new AlgorithmManagerFactory(); + foreach ($jwsAlgorithmProvider->getAvailableAlgorithms() as $algorithm) { + $jwsAlgorithmFactory->add($algorithm->name(), $algorithm); + } + $jwsSerializerFactory = new JWSSerializerManagerFactory(); + $jwsSerializerFactory->add(new CompactSerializer()); + $jwsSerializerFactory->add(new JwsJsonSerializer()); + $jwsSerializerFactory->add(new JwsFlatSerializer()); + + return new \Jose\Component\Signature\JWSLoaderFactory( + $jwsSerializerFactory, + new JWSVerifierFactory($jwsAlgorithmFactory), + null + ); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php new file mode 100644 index 0000000000000..83a3bed54feb5 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php @@ -0,0 +1,194 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Signature\JWSBuilder; +use Jose\Component\Signature\JWSLoaderFactory as LoaderFactory; +use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory as JWSSerializerPool; +use Magento\Framework\Jwt\EncryptionSettingsInterface; +use Magento\Framework\Jwt\Exception\EncryptionException; +use Magento\Framework\Jwt\Exception\JwtException; +use Magento\Framework\Jwt\Exception\MalformedTokenException; +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\Jws\JwsInterface; +use Magento\Framework\Jwt\Jws\JwsSignatureJwks; +use Jose\Component\Core\JWK as AdapterJwk; +use Jose\Component\Core\JWKSet as AdapterJwkSet; + +/** + * Works with JWS. + */ +class JwsManager +{ + /** + * @var JWSBuilder + */ + private $jwsBuilder; + + /** + * @var LoaderFactory + */ + private $jwsLoaderFactory; + + /** + * @var JWSSerializerPool + */ + private $jwsSerializerFactory; + + /** + * @var JwsFactory + */ + private $jwsFactory; + + /** + * @param JwsBuilderFactory $builderFactory + * @param JwsSerializerPoolFactory $serializerPoolFactory + * @param JwsLoaderFactory $jwsLoaderFactory + * @param JwsFactory $jwsFactory + */ + public function __construct( + JwsBuilderFactory $builderFactory, + JwsSerializerPoolFactory $serializerPoolFactory, + JwsLoaderFactory $jwsLoaderFactory, + JwsFactory $jwsFactory + ) { + $this->jwsBuilder = $builderFactory->create(); + $this->jwsSerializerFactory = $serializerPoolFactory->create(); + $this->jwsLoaderFactory = $jwsLoaderFactory->create(); + $this->jwsFactory = $jwsFactory; + } + + /** + * Generate JWS token. + * + * @param JwsInterface $jws + * @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings + * @return string + * @throws JwtException + */ + public function build(JwsInterface $jws, EncryptionSettingsInterface $encryptionSettings): string + { + if (!$encryptionSettings instanceof JwsSignatureJwks) { + throw new JwtException('Can only work with JWK encryption settings for JWS tokens'); + } + $signaturesCount = count($encryptionSettings->getJwkSet()->getKeys()); + if ($jws->getProtectedHeaders() && count($jws->getProtectedHeaders()) !== $signaturesCount) { + throw new MalformedTokenException('Number of headers must equal to number of JWKs'); + } + if ($jws->getUnprotectedHeaders() + && count($jws->getUnprotectedHeaders()) !== $signaturesCount + ) { + throw new MalformedTokenException('There must be an equal number of protected and unprotected headers.'); + } + $builder = $this->jwsBuilder->create(); + $builder = $builder->withPayload($jws->getPayload()->getContent()); + for ($i = 0; $i < $signaturesCount; $i++) { + $jwk = $encryptionSettings->getJwkSet()->getKeys()[$i]; + $alg = $jwk->getAlgorithm(); + if (!$alg) { + throw new EncryptionException('Algorithm is required for JWKs'); + } + $protected = []; + if ($jws->getPayload()->getContentType()) { + $protected['cty'] = $jws->getPayload()->getContentType(); + } + if ($jws->getProtectedHeaders()) { + $protected = $this->extractHeaderData($jws->getProtectedHeaders()[$i]); + } + $protected['alg'] = $alg; + $unprotected = []; + if ($jws->getUnprotectedHeaders()) { + $unprotected = $this->extractHeaderData($jws->getUnprotectedHeaders()[$i]); + } + $builder = $builder->addSignature(new AdapterJwk($jwk->getJsonData()), $protected, $unprotected); + } + $jwsCreated = $builder->build(); + + if ($signaturesCount > 1) { + return $this->jwsSerializerFactory->all()['jws_json_general']->serialize($jwsCreated); + } + if ($jws->getUnprotectedHeaders()) { + return $this->jwsSerializerFactory->all()['jws_json_flattened']->serialize($jwsCreated); + } + return $this->jwsSerializerFactory->all()['jws_compact']->serialize($jwsCreated); + } + + /** + * Read and verify JWS token. + * + * @param string $token + * @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings + * @return JwsInterface + * @throws JwtException + */ + public function read(string $token, EncryptionSettingsInterface $encryptionSettings): JwsInterface + { + if (!$encryptionSettings instanceof JwsSignatureJwks) { + throw new JwtException('Can only work with JWK settings for JWS tokens'); + } + + $loader = $this->jwsLoaderFactory->create( + ['jws_compact', 'jws_json_flattened', 'jws_json_general'], + array_map( + function (Jwk $jwk) { + return $jwk->getAlgorithm(); + }, + $encryptionSettings->getJwkSet()->getKeys() + ) + ); + $jwkSet = new AdapterJwkSet( + array_map( + function (Jwk $jwk) { + return new AdapterJwk($jwk->getJsonData()); + }, + $encryptionSettings->getJwkSet()->getKeys() + ) + ); + try { + $jws = $loader->loadAndVerifyWithKeySet( + $token, + $jwkSet, + $signature, + null + ); + } catch (\Throwable $exception) { + throw new MalformedTokenException('Failed to read JWS token', 0, $exception); + } + if ($signature === null) { + throw new EncryptionException('Failed to verify a JWS token'); + } + $headers = $jws->getSignature($signature); + if ($jws->isPayloadDetached()) { + throw new JwtException('Detached payload is not supported'); + } + + return $this->jwsFactory->create( + $headers->getProtectedHeader(), + $jws->getPayload(), + $headers->getHeader() ? $headers->getHeader() : null + ); + } + + /** + * Extract JOSE header data. + * + * @param HeaderInterface $header + * @return array + */ + private function extractHeaderData(HeaderInterface $header): array + { + $data = []; + foreach ($header->getParameters() as $parameter) { + $data[$parameter->getName()] = $parameter->getValue(); + } + + return $data; + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php new file mode 100644 index 0000000000000..5ad7b39c7f899 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Signature\Serializer\CompactSerializer; +use Jose\Component\Signature\Serializer\JSONFlattenedSerializer; +use Jose\Component\Signature\Serializer\JSONGeneralSerializer; +use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory; + +class JwsSerializerPoolFactory +{ + public function create(): JWSSerializerManagerFactory + { + $jwsSerializerFactory = new JWSSerializerManagerFactory(); + $jwsSerializerFactory->add(new CompactSerializer()); + $jwsSerializerFactory->add(new JSONGeneralSerializer()); + $jwsSerializerFactory->add(new JSONFlattenedSerializer()); + + return $jwsSerializerFactory; + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index fab4f93bcda54..4250b8ceb2fab 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -8,41 +8,16 @@ namespace Magento\JwtFrameworkAdapter\Model; -use Jose\Component\Core\AlgorithmManager; -use Jose\Component\Core\AlgorithmManagerFactory; -use Jose\Component\Signature\JWSBuilder; -use Jose\Component\Signature\JWSVerifierFactory; -use Jose\Component\Signature\Serializer\CompactSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory; -use Jose\Easy\AlgorithmProvider; use Magento\Framework\Jwt\EncryptionSettingsInterface; -use Magento\Framework\Jwt\Exception\EncryptionException; use Magento\Framework\Jwt\Exception\JwtException; use Magento\Framework\Jwt\Exception\MalformedTokenException; -use Magento\Framework\Jwt\HeaderInterface; use Magento\Framework\Jwt\Jwe\JweInterface; use Magento\Framework\Jwt\Jwk; -use Magento\Framework\Jwt\JwkSet; -use Magento\Framework\Jwt\Jws\Jws; -use Magento\Framework\Jwt\Jws\JwsHeader; use Magento\Framework\Jwt\Jws\JwsInterface; -use Magento\Framework\Jwt\Jws\JwsSignatureJwks; use Magento\Framework\Jwt\Jws\JwsSignatureSettingsInterface; use Magento\Framework\Jwt\JwtInterface; use Magento\Framework\Jwt\JwtManagerInterface; -use Magento\Framework\Jwt\Payload\ArbitraryPayload; -use Magento\Framework\Jwt\Payload\ClaimsPayload; -use Magento\Framework\Jwt\Payload\NestedPayload; -use Magento\Framework\Jwt\Payload\NestedPayloadInterface; use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; -use Jose\Component\Core\JWK as AdapterJwk; -use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer; -use Jose\Component\Signature\Serializer\JSONGeneralSerializer as JwsJsonSerializer; -use Jose\Component\Signature\Serializer\JSONFlattenedSerializer as JwsFlatSerializer; -use Jose\Component\Core\JWKSet as AdapterJwkSet; -use Jose\Component\Signature\JWSLoaderFactory; -use Magento\JwtFrameworkAdapter\Model\Data\Claim; -use Magento\JwtFrameworkAdapter\Model\Data\Header; /** * Adapter for jwt-framework. @@ -71,69 +46,16 @@ class JwtManager implements JwtManagerInterface ]; /** - * @var JWSBuilder + * @var JwsManager */ - private $jwsBuilder; + private $jwsManager; /** - * @var JwsCompactSerializer + * @param JwsManager $jwsManager */ - private $jwsCompactSerializer; - - /** - * @var JwsJsonSerializer - */ - private $jwsJsonSerializer; - - /** - * @var JwsFlatSerializer - */ - private $jwsFlatSerializer; - - /** - * @var JWSLoaderFactory - */ - private $jwsLoaderFactory; - - /** - * JwtManager constructor. - */ - public function __construct() + public function __construct(JwsManager $jwsManager) { - $jwsAlgorithms = [ - \Jose\Component\Signature\Algorithm\HS256::class, - \Jose\Component\Signature\Algorithm\HS384::class, - \Jose\Component\Signature\Algorithm\HS512::class, - \Jose\Component\Signature\Algorithm\RS256::class, - \Jose\Component\Signature\Algorithm\RS384::class, - \Jose\Component\Signature\Algorithm\RS512::class, - \Jose\Component\Signature\Algorithm\PS256::class, - \Jose\Component\Signature\Algorithm\PS384::class, - \Jose\Component\Signature\Algorithm\PS512::class, - \Jose\Component\Signature\Algorithm\ES256::class, - \Jose\Component\Signature\Algorithm\ES384::class, - \Jose\Component\Signature\Algorithm\ES512::class, - \Jose\Component\Signature\Algorithm\EdDSA::class, - ]; - $jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms); - $algorithmManager = new AlgorithmManager($jwsAlgorithmProvider->getAvailableAlgorithms()); - $this->jwsBuilder = new JWSBuilder($algorithmManager); - $this->jwsCompactSerializer = new JwsCompactSerializer(); - $this->jwsJsonSerializer = new JwsJsonSerializer(); - $this->jwsFlatSerializer = new JwsFlatSerializer(); - $jwsSerializerFactory = new JWSSerializerManagerFactory(); - $jwsSerializerFactory->add(new CompactSerializer()); - $jwsSerializerFactory->add(new JwsJsonSerializer()); - $jwsSerializerFactory->add(new JwsFlatSerializer()); - $jwsAlgorithmFactory = new AlgorithmManagerFactory(); - foreach ($jwsAlgorithmProvider->getAvailableAlgorithms() as $algorithm) { - $jwsAlgorithmFactory->add($algorithm->name(), $algorithm); - } - $this->jwsLoaderFactory = new JWSLoaderFactory( - $jwsSerializerFactory, - new JWSVerifierFactory($jwsAlgorithmFactory), - null - ); + $this->jwsManager = $jwsManager; } /** @@ -144,8 +66,15 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio if (!$jwt instanceof UnsecuredJwtInterface && !$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) { throw new MalformedTokenException('Can only build JWS, JWE or Unsecured tokens.'); } - if ($jwt instanceof JwsInterface) { - return $this->buildJws($jwt, $encryption); + try { + if ($jwt instanceof JwsInterface) { + return $this->jwsManager->build($jwt, $encryption); + } + } catch (\Throwable $exception) { + if (!$exception instanceof JwtException) { + $exception = new JwtException('Failed to generate a JWT', 0, $exception); + } + throw $exception; } } @@ -162,8 +91,11 @@ public function read(string $token, array $acceptableEncryption): JwtInterface switch ($this->detectJwtType($encryptionSettings)) { case self::JWT_TYPE_JWS: try { - $read = $this->readJws($token, $encryptionSettings); + $read = $this->jwsManager->read($token, $encryptionSettings); } catch (\Throwable $exception) { + if (!$exception instanceof JwtException) { + $exception = new JwtException('Failed to read JWT', 0, $exception); + } $lastException = $exception; } break; @@ -171,106 +103,11 @@ public function read(string $token, array $acceptableEncryption): JwtInterface } if (!$read) { - throw new JwtException('Failed to read JWT', 0, $lastException); + throw $lastException; } return $read; } - /** - * Convert JWK. - * - * @param Jwk $jwk - * @return AdapterJwk - */ - private function convertToAdapterJwk(Jwk $jwk): AdapterJwk - { - return new AdapterJwk($jwk->getJsonData()); - } - - private function convertToAdapterKeySet(JwkSet $jwkSet): AdapterJwkSet - { - return new AdapterJwkSet(array_map([$this, 'convertToAdapterJwk'], $jwkSet->getKeys())); - } - - /** - * Extract JOSE header data. - * - * @param HeaderInterface $header - * @return array - */ - private function extractHeaderData(HeaderInterface $header): array - { - $data = []; - foreach ($header->getParameters() as $parameter) { - $data[$parameter->getName()] = $parameter->getValue(); - } - - return $data; - } - - /** - * Create a JWS. - * - * @param JwsInterface $jws - * @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings - * @return string - * @throws JwtException - */ - private function buildJws(JwsInterface $jws, EncryptionSettingsInterface $encryptionSettings): string - { - if (!$encryptionSettings instanceof JwsSignatureJwks) { - throw new JwtException('Can only work with JWK settings for JWS tokens'); - } - $signaturesCount = count($encryptionSettings->getJwkSet()->getKeys()); - if ($jws->getProtectedHeaders() && count($jws->getProtectedHeaders()) !== $signaturesCount) { - throw new MalformedTokenException('Number of headers must equal to number of JWKs'); - } - if ($jws->getUnprotectedHeaders() - && count($jws->getUnprotectedHeaders()) !== $signaturesCount - ) { - throw new MalformedTokenException('There must be an equal number of protected and unprotected headers.'); - } - - try { - $builder = $this->jwsBuilder->create(); - $builder = $builder->withPayload($jws->getPayload()->getContent()); - for ($i = 0; $i < $signaturesCount; $i++) { - $jwk = $encryptionSettings->getJwkSet()->getKeys()[$i]; - $alg = $jwk->getAlgorithm(); - if (!$alg) { - throw new EncryptionException('Algorithm is required for JWKs'); - } - $protected = []; - if ($jws->getPayload()->getContentType()) { - $protected['cty'] = $jws->getPayload()->getContentType(); - } - if ($jws->getProtectedHeaders()) { - $protected = $this->extractHeaderData($jws->getProtectedHeaders()[$i]); - } - $protected['alg'] = $alg; - $unprotected = []; - if ($jws->getUnprotectedHeaders()) { - $unprotected = $this->extractHeaderData($jws->getUnprotectedHeaders()[$i]); - } - $builder = $builder->addSignature($this->convertToAdapterJwk($jwk), $protected, $unprotected); - } - $jwsCreated = $builder->build(); - - if ($signaturesCount > 1) { - return $this->jwsJsonSerializer->serialize($jwsCreated); - } - if ($jws->getUnprotectedHeaders()) { - return $this->jwsFlatSerializer->serialize($jwsCreated); - } - return $this->jwsCompactSerializer->serialize($jwsCreated); - } catch (\Throwable $exception) { - if (!$exception instanceof JwtException) { - $exception = new JwtException('Something went wrong while generating a JWS', 0, $exception); - } - throw $exception; - } - } - private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): int { if ($encryptionSettings instanceof JwsSignatureSettingsInterface) { @@ -286,68 +123,4 @@ private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): throw new \RuntimeException('Failed to determine JWT type'); } - - /** - * Read and verify a JWS token. - * - * @param string $token - * @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings - * @return JwtInterface - */ - private function readJws(string $token, EncryptionSettingsInterface $encryptionSettings): JwtInterface - { - if (!$encryptionSettings instanceof JwsSignatureJwks) { - throw new JwtException('Can only work with JWK settings for JWS tokens'); - } - - $loader = $this->jwsLoaderFactory->create( - ['jws_compact', 'jws_json_flattened', 'jws_json_general'], - array_map( - function (Jwk $jwk) { - return $jwk->getAlgorithm(); - }, - $encryptionSettings->getJwkSet()->getKeys() - ) - ); - $jws = $loader->loadAndVerifyWithKeySet( - $token, $this->convertToAdapterKeySet($encryptionSettings->getJwkSet()), - $signature, - null - ); - if ($signature === null) { - throw new EncryptionException('Failed to verify a JWS token'); - } - $headers = $jws->getSignature($signature); - $protectedHeaders = []; - foreach ($headers->getProtectedHeader() as $header => $headerValue) { - $protectedHeaders[] = new Header($header, $headerValue, null); - } - $publicHeaders = null; - if ($headers->getHeader()) { - $publicHeaders = []; - foreach ($headers->getHeader() as $header => $headerValue) { - $publicHeaders[] = new Header($header, $headerValue, null); - } - } - if ($jws->isPayloadDetached()) { - throw new JwtException('Detached payload is not supported'); - } - $headersMap = array_merge($headers->getHeader(), $headers->getProtectedHeader()); - if (array_key_exists('cty', $headersMap)) { - if ($headersMap['cty'] === NestedPayloadInterface::CONTENT_TYPE) { - $payload = new NestedPayload($jws->getPayload()); - } else { - $payload = new ArbitraryPayload($jws->getPayload()); - } - } else { - $claimData = json_decode($jws->getPayload(), true); - $claims = []; - foreach ($claimData as $name => $value) { - $claims[] = new Claim($name, $value, null); - } - $payload = new ClaimsPayload($claims); - } - - return new Jws([new JwsHeader($protectedHeaders)], $payload, $publicHeaders ? [new JwsHeader($publicHeaders)] : null); - } } From 020bc364a218534622e7ea7740ad009f9d36ef69 Mon Sep 17 00:00:00 2001 From: Sergiy Vasiutynskyi <s.vasiutynskyi@atwix.com> Date: Wed, 3 Feb 2021 16:07:02 +0200 Subject: [PATCH 029/137] Removed CliCacheFlushActionGroup usage (or changed value) for Wishlist, Swatches, Translation, UrlRewrite, Vault and Weee modules --- .../Test/AdminDisablingSwatchTooltipsTest.xml | 8 ++------ ...frontImageColorWhenFilterByColorFilterTest.xml | 4 +--- .../StorefrontInlineTranslationOnCheckoutTest.xml | 6 ++---- ...eviewsProductImportWithConfigTurnedOffTest.xml | 15 +++------------ ...ewritesForProductInCategoriesSwitchOffTest.xml | 14 +++----------- ...tegoriesTestWithConfigurationTurnedOffTest.xml | 15 +++------------ ...sForProductsWithConfigurationTurnedOffTest.xml | 15 +++------------ ...StorefrontVerifySecureURLRedirectVaultTest.xml | 8 ++------ ...dminFixedTaxValSavedForSpecificWebsiteTest.xml | 8 ++------ ...ProductsToCartFromWishlistUsingSidebarTest.xml | 8 ++------ ...ckOptionsConfigurableProductInWishlistTest.xml | 8 ++------ ...ntDeleteBundleFixedProductFromWishlistTest.xml | 4 +--- ...tDeleteConfigurableProductFromWishlistTest.xml | 4 +--- ...rableProductFromShoppingCartToWishlistTest.xml | 4 +--- ...RemoveProductsFromWishlistUsingSidebarTest.xml | 4 +--- .../Mftf/Test/StorefrontUpdateWishlistTest.xml | 4 +--- ...refrontVerifySecureURLRedirectWishlistTest.xml | 8 ++------ 17 files changed, 32 insertions(+), 105 deletions(-) diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 7647d3ec87a02..0f4c8bebca17d 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -43,9 +43,7 @@ <!-- Enable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 1" stepKey="disableTooltips"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnabling"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterEnabling"/> </after> <!-- Go to the edit page for the "color" attribute --> @@ -149,9 +147,7 @@ <!-- Disable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 0" stepKey="disableTooltips"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisabling"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDisabling"/> <!-- Verify swatch tooltips are not visible --> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml index 734294ba977ba..50963a105efa5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -74,9 +74,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!--Select any option in the Layered navigation and verify product image--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index 4eff032ce160e..6e877baf8ffc2 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -116,7 +116,7 @@ <!-- 2. Refresh magento cache --> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateEnabled"> - <argument name="tags" value=""/> + <argument name="tags" value="full_page"/> </actionGroup> <!-- 3. Go to storefront and click on cart button on the top --> @@ -478,9 +478,7 @@ <!-- 7. Set *Enabled for Storefront* option to *No* and save configuration --> <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> <!-- 8. Clear magento cache --> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateDisabled"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterTranslateDisabled"/> <magentoCLI command="setup:static-content:deploy -f" stepKey="deployStaticContent"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml index 6f7bb6ccb2b84..4833ce686a4e9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml @@ -21,10 +21,7 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites to Yes (default)--> <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterEnableConfig"/> <createData entity="ApiCategory" stepKey="createCategory"> <field key="name">category-admin</field> </createData> @@ -40,10 +37,7 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites to No--> <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDisableConfig"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -61,10 +55,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersIfSet"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewEn"> <argument name="Store" value="customStoreENNotUnique.name"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml index 10b377eebd313..575083dc1d76d 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml @@ -21,10 +21,7 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites--> <comment userInput="Enable config to generate category/product URL Rewrites" stepKey="commentEnableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -39,10 +36,7 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites--> <comment userInput="Enable config to generate category/product URL Rewrites" stepKey="commentEnableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -62,9 +56,7 @@ <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!-- 3. Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- 4. Open Marketing - SEO & Search - URL Rewrites --> <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteAfterDisablingTheConfig"> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml index 78bd397c69289..06d54b10c1402 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml @@ -24,10 +24,7 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites to Yes (default)--> <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterEnableConfig"/> <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> @@ -43,20 +40,14 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites to No--> <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDisableConfig"/> </after> <!-- Steps --> <!-- 1. Log in to Admin --> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml index bfe8a28064496..fc380f433bfbc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml @@ -21,10 +21,7 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites to Yes (default)--> <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterEnableConfig"/> <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> <!-- Create Simple product 1 and assign it to Category 1 --> <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> @@ -33,19 +30,13 @@ <!-- Set the configuration for Generate "category/product" URL Rewrites to No--> <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDisableConfig"/> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> - <!--Flush cache--> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <!-- 1. Log in to Admin --> diff --git a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml index a43d6578925b2..d32525a394d5b 100644 --- a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml +++ b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml @@ -28,15 +28,11 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml index ccbd431848dbc..801ae29b4a8ab 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -54,9 +54,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value="catalog_product_price"/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <!--Set catalog price scope to Global--> @@ -104,9 +102,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value="catalog_product_price"/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!--See available websites only 'All Websites'--> <comment userInput="See available websites 'All Websites', 'Main Website' and Second website" stepKey="commentCheckWebsitesInProductPage"/> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductPageSecondTime"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml index c279adbfe876c..0939d25ab0c21 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml @@ -29,9 +29,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> @@ -41,9 +39,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <!-- Sign in as customer --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml index 638c8f4986a77..da28d5347f820 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml @@ -29,9 +29,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -50,9 +48,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToFirstConfigProductPage"> <argument name="productId" value="$$createFirstConfigProduct.id$$"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml index 31bc9f6a31de7..a5459a79e7d26 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml @@ -48,9 +48,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml index da2cec8284c46..5c9a7f6a02274 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml @@ -108,9 +108,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml index 05a42314ddb71..b6e57d794e4a6 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml @@ -110,9 +110,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml index b2364b72f7db8..ce66f44587d11 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml @@ -39,9 +39,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index 86d09783e0f55..5de95885ebb7b 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -30,9 +30,7 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml index f5958f5efd414..57865f10b305e 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml @@ -28,15 +28,11 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> From 1376ca40fc0b32a0c3aa86d11dd2d0c78edb4d59 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Wed, 3 Feb 2021 12:31:32 -0600 Subject: [PATCH 030/137] MC-38539: Introduce JWT wrapper --- .../JwtFrameworkAdapter/Model/Data/Header.php | 3 +- .../Model/JweAlgorithmManagerFactory.php | 39 ++++ .../Model/JweBuilderFactory.php | 54 +++++ .../Model/JweCompressionManagerFactory.php | 20 ++ .../JweContentAlgorithmManagerFactory.php | 29 +++ .../JwtFrameworkAdapter/Model/JweFactory.php | 76 +++++++ .../Model/JweLoaderFactory.php | 63 ++++++ .../JwtFrameworkAdapter/Model/JweManager.php | 199 ++++++++++++++++++ .../Model/JweSerializerPoolFactory.php | 28 +++ .../Model/JwsAlgorithmManagerFactory.php | 37 ++++ .../Model/JwsBuilderFactory.php | 30 +-- .../Model/JwsLoaderFactory.php | 62 +++--- .../JwtFrameworkAdapter/Model/JwsManager.php | 33 ++- .../Model/JwsSerializerPoolFactory.php | 17 +- .../JwtFrameworkAdapter/Model/JwtManager.php | 57 ++++- .../Magento/Framework/Jwt/JwtManagerTest.php | 75 ++++++- .../Magento/Framework/Jwt/Jwe/Jwe.php | 96 +++++++++ .../Framework/Jwt/Jwe/JweEncryptionJwks.php | 91 ++++++++ .../Jwe/JweEncryptionSettingsInterface.php | 38 ++++ .../Magento/Framework/Jwt/Jwe/JweHeader.php | 52 +++++ .../Framework/Jwt/Jwe/JweInterface.php | 2 +- lib/internal/Magento/Framework/Jwt/Jwk.php | 32 +++ .../Magento/Framework/Jwt/JwkFactory.php | 23 +- .../Framework/Jwt/Jws/JwsSignatureJwks.php | 2 +- 24 files changed, 1050 insertions(+), 108 deletions(-) create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweBuilderFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweLoaderFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JweSerializerPoolFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/Jwe.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionJwks.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionSettingsInterface.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jwe/JweHeader.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php index 9c2c90bb9cd3d..0916326e1865b 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Header.php @@ -8,9 +8,10 @@ namespace Magento\JwtFrameworkAdapter\Model\Data; +use Magento\Framework\Jwt\Jwe\JweHeaderParameterInterface; use Magento\Framework\Jwt\Jws\JwsHeaderParameterInterface; -class Header implements JwsHeaderParameterInterface +class Header implements JwsHeaderParameterInterface, JweHeaderParameterInterface { /** * @var string diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php new file mode 100644 index 0000000000000..7ae4643396bae --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManager; +use Jose\Easy\AlgorithmProvider; + +class JweAlgorithmManagerFactory +{ + private const ALGOS = [ + \Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\A128KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\A192KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\Dir::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA128KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA192KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA256KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\A128GCMKW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\A192GCMKW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\A256GCMKW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS256A128KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS384A192KW::class, + \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS512A256KW::class + ]; + + public function create(): AlgorithmManager + { + return new AlgorithmManager((new AlgorithmProvider(self::ALGOS))->getAvailableAlgorithms()); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweBuilderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweBuilderFactory.php new file mode 100644 index 0000000000000..69a9a90dbaa33 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweBuilderFactory.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Encryption\Compression\CompressionMethodManager; +use Jose\Component\Encryption\JWEBuilder; +use Jose\Component\Encryption\Serializer\JWESerializerManager; + +class JweBuilderFactory +{ + /** + * @var JWESerializerManager + */ + private $serializers; + + /** + * @var AlgorithmManager + */ + private $algoManager; + + /** + * @var AlgorithmManager + */ + private $contentAlgoManager; + + /** + * @var CompressionMethodManager + */ + private $compressionManager; + + public function __construct( + JweSerializerPoolFactory $serializerPoolFactory, + JweAlgorithmManagerFactory $algorithmManagerFactory, + JweContentAlgorithmManagerFactory $contentAlgoManagerFactory, + JweCompressionManagerFactory $compressionManagerFactory + ) { + $this->serializers = $serializerPoolFactory->create(); + $this->algoManager = $algorithmManagerFactory->create(); + $this->contentAlgoManager = $contentAlgoManagerFactory->create(); + $this->compressionManager = $compressionManagerFactory->create(); + } + + public function create(): JWEBuilder + { + return new JWEBuilder($this->algoManager, $this->contentAlgoManager, $this->compressionManager); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php new file mode 100644 index 0000000000000..16367cff6a534 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Encryption\Compression\CompressionMethodManager; +use Jose\Component\Encryption\Compression\Deflate; + +class JweCompressionManagerFactory +{ + public function create(): CompressionMethodManager + { + return new CompressionMethodManager([new Deflate()]); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php new file mode 100644 index 0000000000000..7c5db44b4fd9b --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManager; +use Jose\Easy\AlgorithmProvider; + +class JweContentAlgorithmManagerFactory +{ + private const ALGOS = [ + \Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256::class, + \Jose\Component\Encryption\Algorithm\ContentEncryption\A192CBCHS384::class, + \Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512::class, + \Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM::class, + \Jose\Component\Encryption\Algorithm\ContentEncryption\A192GCM::class, + \Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM::class, + ]; + + public function create(): AlgorithmManager + { + return new AlgorithmManager((new AlgorithmProvider(self::ALGOS))->getAvailableAlgorithms()); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweFactory.php new file mode 100644 index 0000000000000..07a99c202b3ef --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweFactory.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Magento\Framework\Jwt\Jwe\Jwe; +use Magento\Framework\Jwt\Jwe\JweHeader; +use Magento\Framework\Jwt\Jwe\JweInterface; +use Magento\Framework\Jwt\Jws\Jws; +use Magento\Framework\Jwt\Jws\JwsHeader; +use Magento\Framework\Jwt\Jws\JwsInterface; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayload; +use Magento\Framework\Jwt\Payload\NestedPayload; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\JwtFrameworkAdapter\Model\Data\Claim; +use Magento\JwtFrameworkAdapter\Model\Data\Header; + +/** + * Create JWE data object. + */ +class JweFactory +{ + public function create( + array $protectedHeadersMap, + string $payload, + ?array $unprotectedHeadersMap, + ?array $recipientHeadersMap + ): JweInterface { + $protectedHeaders = []; + foreach ($protectedHeadersMap as $header => $headerValue) { + $protectedHeaders[] = new Header($header, $headerValue, null); + } + $publicHeaders = null; + if ($unprotectedHeadersMap) { + $publicHeaders = []; + foreach ($unprotectedHeadersMap as $header => $headerValue) { + $publicHeaders[] = new Header($header, $headerValue, null); + } + } + $recipientHeader = null; + if ($recipientHeadersMap) { + $recipientHeader = []; + foreach ($recipientHeadersMap as $header => $headerValue) { + $recipientHeader[] = new Header($header, $headerValue, null); + } + } + $headersMap = array_merge($unprotectedHeadersMap ?? [], $recipientHeader ?? [], $protectedHeadersMap); + if (array_key_exists('cty', $headersMap)) { + if ($headersMap['cty'] === NestedPayloadInterface::CONTENT_TYPE) { + $payload = new NestedPayload($payload); + } else { + $payload = new ArbitraryPayload($payload); + } + } else { + $claimData = json_decode($payload, true); + $claims = []; + foreach ($claimData as $name => $value) { + $claims[] = new Claim($name, $value, null); + } + $payload = new ClaimsPayload($claims); + } + + return new Jwe( + new JweHeader($protectedHeaders), + $publicHeaders ? new JweHeader($publicHeaders) : null, + $recipientHeader ? [new JweHeader($recipientHeader)] : null, + $payload + ); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweLoaderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweLoaderFactory.php new file mode 100644 index 0000000000000..25941e33b15cb --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweLoaderFactory.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Encryption\Compression\CompressionMethodManager; +use Jose\Component\Encryption\JWEDecrypter; +use Jose\Component\Encryption\JWELoader; +use Jose\Component\Encryption\Serializer\JWESerializerManager; + +class JweLoaderFactory +{ + /** + * @var JWESerializerManager + */ + private $serializers; + + /** + * @var AlgorithmManager + */ + private $algoManager; + + /** + * @var AlgorithmManager + */ + private $contentAlgoManager; + + /** + * @var CompressionMethodManager + */ + private $compressionManager; + + public function __construct( + JweSerializerPoolFactory $serializerPoolFactory, + JweAlgorithmManagerFactory $algorithmManagerFactory, + JweContentAlgorithmManagerFactory $contentAlgoManagerFactory, + JweCompressionManagerFactory $compressionManagerFactory + ) { + $this->serializers = $serializerPoolFactory->create(); + $this->algoManager = $algorithmManagerFactory->create(); + $this->contentAlgoManager = $contentAlgoManagerFactory->create(); + $this->compressionManager = $compressionManagerFactory->create(); + } + + public function create(): JWELoader + { + return new JWELoader( + $this->serializers, + new JWEDecrypter( + $this->algoManager, + $this->contentAlgoManager, + $this->compressionManager + ), + null + ); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php new file mode 100644 index 0000000000000..be520953a9138 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php @@ -0,0 +1,199 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Encryption\JWEBuilder; +use Jose\Component\Encryption\JWELoader; +use Jose\Component\Encryption\Serializer\JWESerializerManager; +use Magento\Framework\Jwt\EncryptionSettingsInterface; +use Magento\Framework\Jwt\Exception\EncryptionException; +use Magento\Framework\Jwt\Exception\JwtException; +use Magento\Framework\Jwt\Exception\MalformedTokenException; +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\Jwe\JweEncryptionJwks; +use Magento\Framework\Jwt\Jwe\JweInterface; +use Jose\Component\Core\JWK as AdapterJwk; +use Jose\Component\Core\JWKSet as AdapterJwkSet; +use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; + +/** + * Works with JWE + */ +class JweManager +{ + /** + * @var JWEBuilder + */ + private $builder; + + /** + * @var JWESerializerManager + */ + private $serializer; + + /** + * @var JWELoader + */ + private $loader; + + /** + * @var JweFactory + */ + private $jweFactory; + + public function __construct( + JweBuilderFactory $jweBuilderFactory, + JweSerializerPoolFactory $serializerPoolFactory, + JweLoaderFactory $jweLoaderFactory, + JweFactory $jweFactory + ) { + $this->builder = $jweBuilderFactory->create(); + $this->serializer = $serializerPoolFactory->create(); + $this->loader = $jweLoaderFactory->create(); + $this->jweFactory = $jweFactory; + } + + /** + * Generate JWE token. + * + * @param JweInterface $jwe + * @param EncryptionSettingsInterface|JweEncryptionJwks $encryptionSettings + * @return string + */ + public function build(JweInterface $jwe, EncryptionSettingsInterface $encryptionSettings): string + { + $this->validateJweSettings($jwe, $encryptionSettings); + + $builder = $this->builder->create(); + + $payload = $jwe->getPayload(); + $builder = $builder->withPayload($payload->getContent()); + + $sharedProtected = $this->extractHeaderData($jwe->getProtectedHeader()); + if (!$jwe->getPerRecipientUnprotectedHeaders()) { + $sharedProtected['enc'] = $encryptionSettings->getContentEncryptionAlgorithm(); + } + if (!$jwe->getPerRecipientUnprotectedHeaders()) { + $sharedProtected['alg'] = $encryptionSettings->getAlgorithmName(); + } + if ($payload instanceof ClaimsPayloadInterface) { + foreach ($payload->getClaims() as $claim) { + if ($claim->isHeaderDuplicated()) { + $sharedProtected[$claim->getName()] = $claim->getValue(); + } + } + } + $builder = $builder->withSharedProtectedHeader($sharedProtected); + + $sharedUnprotected = []; + if ($jwe->getSharedUnprotectedHeader()) { + $sharedUnprotected = array_merge( + $this->extractHeaderData($jwe->getSharedUnprotectedHeader()), + $sharedUnprotected + ); + } + if ($sharedUnprotected) { + $builder = $builder->withSharedHeader($sharedUnprotected); + } + + if (!$jwe->getPerRecipientUnprotectedHeaders()) { + $builder = $builder->addRecipient( + new AdapterJwk($encryptionSettings->getJwkSet()->getKeys()[0]->getJsonData()) + ); + } else { + foreach ($jwe->getPerRecipientUnprotectedHeaders() as $i => $header) { + $jwk = $encryptionSettings->getJwkSet()->getKeys()[$i]; + $headerData = $this->extractHeaderData($header); + $headerData['alg'] = $jwk->getAlgorithm(); + $builder = $builder->addRecipient(new AdapterJwk($jwk->getJsonData()), $headerData); + } + } + + $built = $builder->build(); + if ($jwe->getPerRecipientUnprotectedHeaders() && count($jwe->getPerRecipientUnprotectedHeaders()) === 1) { + return $this->serializer->serialize('jwe_json_flattened', $built); + } + if ($jwe->getPerRecipientUnprotectedHeaders()) { + return $this->serializer->serialize('jwe_json_general', $built); + } + return $this->serializer->serialize('jwe_compact', $built); + } + + /** + * Read JWE token. + * + * @param string $token + * @param EncryptionSettingsInterface|JweEncryptionJwks $encryptionSettings + * @return JweInterface + */ + public function read(string $token, EncryptionSettingsInterface $encryptionSettings): JweInterface + { + if (!$encryptionSettings instanceof JweEncryptionJwks) { + throw new JwtException('Can only work with JWK encryption settings for JWE tokens'); + } + + $jwkSet = new AdapterJwkSet( + array_map( + function (Jwk $jwk) { + return new AdapterJwk($jwk->getJsonData()); + }, + $encryptionSettings->getJwkSet()->getKeys() + ) + ); + try { + /** @var int|null $recipientId */ + $jwe = $this->loader->loadAndDecryptWithKeySet($token, $jwkSet, $recipientId); + } catch (\Throwable $exception) { + throw new EncryptionException('Failed to decrypt JWE token.', 0, $exception); + } + if ($recipientId) { + throw new EncryptionException('Failed to decrypt JWE token.'); + } + $recipientHeader = $jwe->getRecipient($recipientId)->getHeader(); + + return $this->jweFactory->create( + $jwe->getSharedProtectedHeader(), + $jwe->getPayload() ?? '', + $jwe->getSharedHeader() ? $jwe->getSharedHeader() : null, + $recipientHeader ? $recipientHeader : null + ); + } + + private function validateJweSettings(JweInterface $jwe, EncryptionSettingsInterface $encryptionSettings): void + { + if (!$encryptionSettings instanceof JweEncryptionJwks) { + throw new JwtException('Can only work with JWK encryption settings for JWE tokens'); + } + if ($jwe->getPerRecipientUnprotectedHeaders() + && count($encryptionSettings->getJwkSet()->getKeys()) !== count($jwe->getPerRecipientUnprotectedHeaders()) + ) { + throw new EncryptionException('Not enough JWKs to encrypt all headers'); + } + if (count($encryptionSettings->getJwkSet()->getKeys()) > 1 && !$jwe->getPerRecipientUnprotectedHeaders()) { + throw new MalformedTokenException('Need more per-recipient headers for the amount of keys'); + } + } + + /** + * Extract JOSE header data. + * + * @param HeaderInterface $header + * @return array + */ + private function extractHeaderData(HeaderInterface $header): array + { + $data = []; + foreach ($header->getParameters() as $parameter) { + $data[$parameter->getName()] = $parameter->getValue(); + } + + return $data; + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweSerializerPoolFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweSerializerPoolFactory.php new file mode 100644 index 0000000000000..25418af169ed6 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweSerializerPoolFactory.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Encryption\Serializer\CompactSerializer; +use Jose\Component\Encryption\Serializer\JSONFlattenedSerializer; +use Jose\Component\Encryption\Serializer\JSONGeneralSerializer; +use Jose\Component\Encryption\Serializer\JWESerializerManager; + +class JweSerializerPoolFactory +{ + public function create(): JWESerializerManager + { + return new JWESerializerManager( + [ + new CompactSerializer(), + new JSONGeneralSerializer(), + new JSONFlattenedSerializer() + ] + ); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php new file mode 100644 index 0000000000000..1c0a90ffe862e --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Core\AlgorithmManager; +use Jose\Easy\AlgorithmProvider; + +class JwsAlgorithmManagerFactory +{ + private const ALGOS = [ + + \Jose\Component\Signature\Algorithm\HS256::class, + \Jose\Component\Signature\Algorithm\HS384::class, + \Jose\Component\Signature\Algorithm\HS512::class, + \Jose\Component\Signature\Algorithm\RS256::class, + \Jose\Component\Signature\Algorithm\RS384::class, + \Jose\Component\Signature\Algorithm\RS512::class, + \Jose\Component\Signature\Algorithm\PS256::class, + \Jose\Component\Signature\Algorithm\PS384::class, + \Jose\Component\Signature\Algorithm\PS512::class, + \Jose\Component\Signature\Algorithm\ES256::class, + \Jose\Component\Signature\Algorithm\ES384::class, + \Jose\Component\Signature\Algorithm\ES512::class, + \Jose\Component\Signature\Algorithm\EdDSA::class + ]; + + public function create(): AlgorithmManager + { + return new AlgorithmManager((new AlgorithmProvider(self::ALGOS))->getAvailableAlgorithms()); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php index cb9801defd19f..f851b0cf0a438 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsBuilderFactory.php @@ -10,30 +10,20 @@ use Jose\Component\Core\AlgorithmManager; use Jose\Component\Signature\JWSBuilder; -use Jose\Easy\AlgorithmProvider; class JwsBuilderFactory { + /** + * @var AlgorithmManager + */ + private $algoManager; + + public function __construct(JwsAlgorithmManagerFactory $algorithmManagerFactory) { + $this->algoManager = $algorithmManagerFactory->create(); + } + public function create(): JWSBuilder { - $jwsAlgorithms = [ - \Jose\Component\Signature\Algorithm\HS256::class, - \Jose\Component\Signature\Algorithm\HS384::class, - \Jose\Component\Signature\Algorithm\HS512::class, - \Jose\Component\Signature\Algorithm\RS256::class, - \Jose\Component\Signature\Algorithm\RS384::class, - \Jose\Component\Signature\Algorithm\RS512::class, - \Jose\Component\Signature\Algorithm\PS256::class, - \Jose\Component\Signature\Algorithm\PS384::class, - \Jose\Component\Signature\Algorithm\PS512::class, - \Jose\Component\Signature\Algorithm\ES256::class, - \Jose\Component\Signature\Algorithm\ES384::class, - \Jose\Component\Signature\Algorithm\ES512::class, - \Jose\Component\Signature\Algorithm\EdDSA::class, - ]; - $jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms); - $algorithmManager = new AlgorithmManager($jwsAlgorithmProvider->getAvailableAlgorithms()); - - return new JWSBuilder($algorithmManager); + return new JWSBuilder($this->algoManager); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php index aba88f89207b0..ff67f0ac8c1a6 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsLoaderFactory.php @@ -8,46 +8,36 @@ namespace Magento\JwtFrameworkAdapter\Model; -use Jose\Component\Core\AlgorithmManagerFactory; -use Jose\Component\Signature\JWSVerifierFactory; -use Jose\Component\Signature\Serializer\CompactSerializer; -use Jose\Component\Signature\Serializer\JSONFlattenedSerializer as JwsFlatSerializer; -use Jose\Component\Signature\Serializer\JSONGeneralSerializer as JwsJsonSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory; -use Jose\Easy\AlgorithmProvider; +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Signature\JWSLoader; +use Jose\Component\Signature\JWSVerifier; +use Jose\Component\Signature\Serializer\JWSSerializerManager; class JwsLoaderFactory { - public function create() - { - $jwsAlgorithms = [ - \Jose\Component\Signature\Algorithm\HS256::class, - \Jose\Component\Signature\Algorithm\HS384::class, - \Jose\Component\Signature\Algorithm\HS512::class, - \Jose\Component\Signature\Algorithm\RS256::class, - \Jose\Component\Signature\Algorithm\RS384::class, - \Jose\Component\Signature\Algorithm\RS512::class, - \Jose\Component\Signature\Algorithm\PS256::class, - \Jose\Component\Signature\Algorithm\PS384::class, - \Jose\Component\Signature\Algorithm\PS512::class, - \Jose\Component\Signature\Algorithm\ES256::class, - \Jose\Component\Signature\Algorithm\ES384::class, - \Jose\Component\Signature\Algorithm\ES512::class, - \Jose\Component\Signature\Algorithm\EdDSA::class, - ]; - $jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms); - $jwsAlgorithmFactory = new AlgorithmManagerFactory(); - foreach ($jwsAlgorithmProvider->getAvailableAlgorithms() as $algorithm) { - $jwsAlgorithmFactory->add($algorithm->name(), $algorithm); - } - $jwsSerializerFactory = new JWSSerializerManagerFactory(); - $jwsSerializerFactory->add(new CompactSerializer()); - $jwsSerializerFactory->add(new JwsJsonSerializer()); - $jwsSerializerFactory->add(new JwsFlatSerializer()); + /** + * @var JWSSerializerManager + */ + private $serializer; + + /** + * @var AlgorithmManager + */ + private $algoManager; - return new \Jose\Component\Signature\JWSLoaderFactory( - $jwsSerializerFactory, - new JWSVerifierFactory($jwsAlgorithmFactory), + public function __construct( + JwsSerializerPoolFactory $serializerPoolFactory, + JwsAlgorithmManagerFactory $algorithmManagerFactory + ) { + $this->serializer = $serializerPoolFactory->create(); + $this->algoManager = $algorithmManagerFactory->create(); + } + + public function create(): JWSLoader + { + return new JWSLoader( + $this->serializer, + new JWSVerifier($this->algoManager), null ); } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php index 83a3bed54feb5..dbe8852a19d2d 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php @@ -9,8 +9,8 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Signature\JWSBuilder; -use Jose\Component\Signature\JWSLoaderFactory as LoaderFactory; -use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory as JWSSerializerPool; +use Jose\Component\Signature\JWSLoader; +use Jose\Component\Signature\Serializer\JWSSerializerManager; use Magento\Framework\Jwt\EncryptionSettingsInterface; use Magento\Framework\Jwt\Exception\EncryptionException; use Magento\Framework\Jwt\Exception\JwtException; @@ -33,14 +33,14 @@ class JwsManager private $jwsBuilder; /** - * @var LoaderFactory + * @var JWSLoader */ - private $jwsLoaderFactory; + private $jwsLoader; /** - * @var JWSSerializerPool + * @var JWSSerializerManager */ - private $jwsSerializerFactory; + private $jwsSerializer; /** * @var JwsFactory @@ -60,8 +60,8 @@ public function __construct( JwsFactory $jwsFactory ) { $this->jwsBuilder = $builderFactory->create(); - $this->jwsSerializerFactory = $serializerPoolFactory->create(); - $this->jwsLoaderFactory = $jwsLoaderFactory->create(); + $this->jwsSerializer = $serializerPoolFactory->create(); + $this->jwsLoader = $jwsLoaderFactory->create(); $this->jwsFactory = $jwsFactory; } @@ -112,12 +112,12 @@ public function build(JwsInterface $jws, EncryptionSettingsInterface $encryption $jwsCreated = $builder->build(); if ($signaturesCount > 1) { - return $this->jwsSerializerFactory->all()['jws_json_general']->serialize($jwsCreated); + return $this->jwsSerializer->serialize('jws_json_general', $jwsCreated); } if ($jws->getUnprotectedHeaders()) { - return $this->jwsSerializerFactory->all()['jws_json_flattened']->serialize($jwsCreated); + return $this->jwsSerializer->serialize('jws_json_flattened', $jwsCreated); } - return $this->jwsSerializerFactory->all()['jws_compact']->serialize($jwsCreated); + return $this->jwsSerializer->serialize('jws_compact', $jwsCreated); } /** @@ -134,15 +134,6 @@ public function read(string $token, EncryptionSettingsInterface $encryptionSetti throw new JwtException('Can only work with JWK settings for JWS tokens'); } - $loader = $this->jwsLoaderFactory->create( - ['jws_compact', 'jws_json_flattened', 'jws_json_general'], - array_map( - function (Jwk $jwk) { - return $jwk->getAlgorithm(); - }, - $encryptionSettings->getJwkSet()->getKeys() - ) - ); $jwkSet = new AdapterJwkSet( array_map( function (Jwk $jwk) { @@ -152,7 +143,7 @@ function (Jwk $jwk) { ) ); try { - $jws = $loader->loadAndVerifyWithKeySet( + $jws = $this->jwsLoader->loadAndVerifyWithKeySet( $token, $jwkSet, $signature, diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php index 5ad7b39c7f899..70306829b90a3 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsSerializerPoolFactory.php @@ -11,17 +11,18 @@ use Jose\Component\Signature\Serializer\CompactSerializer; use Jose\Component\Signature\Serializer\JSONFlattenedSerializer; use Jose\Component\Signature\Serializer\JSONGeneralSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory; +use Jose\Component\Signature\Serializer\JWSSerializerManager; class JwsSerializerPoolFactory { - public function create(): JWSSerializerManagerFactory + public function create(): JWSSerializerManager { - $jwsSerializerFactory = new JWSSerializerManagerFactory(); - $jwsSerializerFactory->add(new CompactSerializer()); - $jwsSerializerFactory->add(new JSONGeneralSerializer()); - $jwsSerializerFactory->add(new JSONFlattenedSerializer()); - - return $jwsSerializerFactory; + return new JWSSerializerManager( + [ + new CompactSerializer(), + new JSONGeneralSerializer(), + new JSONFlattenedSerializer() + ] + ); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index 4250b8ceb2fab..d1db17222e318 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -45,17 +45,43 @@ class JwtManager implements JwtManagerInterface Jwk::ALGORITHM_PS512 ]; + private const JWE_ALGORITHMS = [ + Jwk::ALGORITHM_RSA_OAEP, + Jwk::ALGORITHM_RSA_OAEP_256, + Jwk::ALGORITHM_A128KW, + Jwk::ALGORITHM_A192KW, + Jwk::ALGORITHM_A256KW, + Jwk::ALGORITHM_DIR, + Jwk::ALGORITHM_ECDH_ES, + Jwk::ALGORITHM_ECDH_ES_A128KW, + Jwk::ALGORITHM_ECDH_ES_A192KW, + Jwk::ALGORITHM_ECDH_ES_A256KW, + Jwk::ALGORITHM_A128GCMKW, + Jwk::ALGORITHM_A192GCMKW, + Jwk::ALGORITHM_A256GCMKW, + Jwk::ALGORITHM_PBES2_HS256_A128KW, + Jwk::ALGORITHM_PBES2_HS384_A192KW, + Jwk::ALGORITHM_PBES2_HS512_A256KW, + ]; + /** * @var JwsManager */ private $jwsManager; + /** + * @var JweManager + */ + private $jweManager; + /** * @param JwsManager $jwsManager + * @param JweManager $jweManager */ - public function __construct(JwsManager $jwsManager) + public function __construct(JwsManager $jwsManager, JweManager $jweManager) { $this->jwsManager = $jwsManager; + $this->jweManager = $jweManager; } /** @@ -70,6 +96,9 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio if ($jwt instanceof JwsInterface) { return $this->jwsManager->build($jwt, $encryption); } + if ($jwt instanceof JweInterface) { + return $this->jweManager->build($jwt, $encryption); + } } catch (\Throwable $exception) { if (!$exception instanceof JwtException) { $exception = new JwtException('Failed to generate a JWT', 0, $exception); @@ -88,17 +117,20 @@ public function read(string $token, array $acceptableEncryption): JwtInterface /** @var \Throwable|null $lastException */ $lastException = null; foreach ($acceptableEncryption as $encryptionSettings) { - switch ($this->detectJwtType($encryptionSettings)) { - case self::JWT_TYPE_JWS: - try { + try { + switch ($this->detectJwtType($encryptionSettings)) { + case self::JWT_TYPE_JWS: $read = $this->jwsManager->read($token, $encryptionSettings); - } catch (\Throwable $exception) { - if (!$exception instanceof JwtException) { - $exception = new JwtException('Failed to read JWT', 0, $exception); - } - $lastException = $exception; - } - break; + break; + case self::JWT_TYPE_JWE: + $read = $this->jweManager->read($token, $encryptionSettings); + break; + } + } catch (\Throwable $exception) { + if (!$exception instanceof JwtException) { + $exception = new JwtException('Failed to read JWT', 0, $exception); + } + $lastException = $exception; } } @@ -120,6 +152,9 @@ private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): if (in_array($encryptionSettings->getAlgorithmName(), self::JWS_ALGORITHMS, true)) { return self::JWT_TYPE_JWS; } + if (in_array($encryptionSettings->getAlgorithmName(), self::JWE_ALGORITHMS, true)) { + return self::JWT_TYPE_JWE; + } throw new \RuntimeException('Failed to determine JWT type'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 339ef21121834..9895318ba91c2 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -17,6 +17,10 @@ use Magento\Framework\Jwt\Header\Critical; use Magento\Framework\Jwt\Header\PrivateHeaderParameter; use Magento\Framework\Jwt\Header\PublicHeaderParameter; +use Magento\Framework\Jwt\Jwe\Jwe; +use Magento\Framework\Jwt\Jwe\JweEncryptionJwks; +use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface; +use Magento\Framework\Jwt\Jwe\JweHeader; use Magento\Framework\Jwt\Jwe\JweInterface; use Magento\Framework\Jwt\Jws\Jws; use Magento\Framework\Jwt\Jws\JwsHeader; @@ -63,7 +67,10 @@ public function testCreateRead( $recreated = $this->manager->read($token, $readEncryption); //Verifying header - if ((!$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) || count($jwt->getProtectedHeaders()) == 1) { + if ((!$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) + || ($jwt instanceof JwsInterface && count($jwt->getProtectedHeaders()) == 1) + || ($jwt instanceof JweInterface && !$jwt->getPerRecipientUnprotectedHeaders()) + ) { $this->verifyAgainstHeaders([$jwt->getHeader()], $recreated->getHeader()); } //Verifying payload @@ -86,9 +93,39 @@ public function testCreateRead( $this->verifyAgainstHeaders($jwt->getUnprotectedHeaders(), $recreated->getUnprotectedHeaders()[0]); } $this->verifyAgainstHeaders($jwt->getProtectedHeaders(), $recreated->getProtectedHeaders()[0]); - } - if ($jwt instanceof JweInterface) { + } elseif ($jwt instanceof JweInterface) { $this->assertInstanceOf(JweInterface::class, $recreated); + /** @var JweInterface $recreated */ + if (!$jwt->getPerRecipientUnprotectedHeaders()) { + $this->assertNull($recreated->getPerRecipientUnprotectedHeaders()); + } else { + $this->assertTrue(count($recreated->getPerRecipientUnprotectedHeaders()) >= 1); + $this->verifyAgainstHeaders( + $jwt->getPerRecipientUnprotectedHeaders(), + $recreated->getPerRecipientUnprotectedHeaders()[0] + ); + } + if (!$jwt->getSharedUnprotectedHeader()) { + $this->assertNull($recreated->getSharedUnprotectedHeader()); + } else { + $this->verifyAgainstHeaders( + [$jwt->getSharedUnprotectedHeader()], + $recreated->getSharedUnprotectedHeader() + ); + } + $this->verifyAgainstHeaders([$jwt->getProtectedHeader()], $recreated->getProtectedHeader()); + $payload = $jwt->getPayload(); + if ($payload instanceof ClaimsPayloadInterface) { + foreach ($payload->getClaims() as $claim) { + $header = $recreated->getProtectedHeader()->getParameter($claim->getName()); + if ($claim->isHeaderDuplicated()) { + $this->assertNotNull($header); + $this->assertEquals($claim->getValue(), $header->getValue()); + } else { + $this->assertNull($header); + } + } + } } if ($jwt instanceof UnsecuredJwtInterface) { $this->assertInstanceOf(UnsecuredJwtInterface::class, $recreated); @@ -170,6 +207,25 @@ public function getTokenVariants(): array new JwsHeader([new PrivateHeaderParameter('public2', 'header')]) ] ); + $flatJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + null, + null, + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); //Keys [$rsaPrivate, $rsaPublic] = $this->createRsaKeys(); @@ -267,6 +323,19 @@ public function getTokenVariants(): array new JwsSignatureJwks($jwkFactory->createSignPs512($rsaPrivate, 'pass')), [new JwsSignatureJwks($jwkFactory->createVerifyPs512($rsaPublic))] ], + 'jwe-A128KW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createA128KW($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createA128KW($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ] ]; } diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/Jwe.php b/lib/internal/Magento/Framework/Jwt/Jwe/Jwe.php new file mode 100644 index 0000000000000..8b10244fb6f87 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/Jwe.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jwe; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\PayloadInterface; + +/** + * JWE DTO. + */ +class Jwe implements JweInterface +{ + /** + * @var HeaderInterface + */ + private $protectedHeader; + + /** + * @var HeaderInterface|null + */ + private $unprotectedHeader; + + /** + * @var HeaderInterface[]|null + */ + private $recipientHeaders; + + /** + * @var PayloadInterface + */ + private $payload; + + /** + * @param HeaderInterface $protectedHeader + * @param HeaderInterface|null $unprotectedHeader + * @param HeaderInterface[]|null $recipientHeaders + * @param PayloadInterface $payload + */ + public function __construct( + HeaderInterface $protectedHeader, + ?HeaderInterface $unprotectedHeader, + ?array $recipientHeaders, + PayloadInterface $payload + ) { + $this->protectedHeader = $protectedHeader; + $this->unprotectedHeader = $unprotectedHeader; + $this->recipientHeaders = $recipientHeaders ? array_values($recipientHeaders) : null; + $this->payload = $payload; + } + + /** + * @inheritDoc + */ + public function getProtectedHeader(): HeaderInterface + { + return $this->protectedHeader; + } + + /** + * @inheritDoc + */ + public function getSharedUnprotectedHeader(): ?HeaderInterface + { + return $this->unprotectedHeader; + } + + /** + * @inheritDoc + */ + public function getPerRecipientUnprotectedHeaders(): ?array + { + return $this->recipientHeaders; + } + + /** + * @inheritDoc + */ + public function getHeader(): HeaderInterface + { + return $this->getProtectedHeader(); + } + + /** + * @inheritDoc + */ + public function getPayload(): PayloadInterface + { + return $this->payload; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionJwks.php b/lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionJwks.php new file mode 100644 index 0000000000000..d3d3614053d7c --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionJwks.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jwe; + +use Magento\Framework\Jwt\Exception\EncryptionException; +use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\JwkSet; + +/** + * JWK encryption settings. + */ +class JweEncryptionJwks implements JweEncryptionSettingsInterface +{ + /** + * @var JwkSet + */ + private $jwkSet; + + /** + * @var string + */ + private $contentAlgo; + + /** + * @param JwkSet|Jwk $jwk + * @param string $contentEncryptionAlgo + */ + public function __construct($jwk, string $contentEncryptionAlgo) + { + if ($jwk instanceof Jwk) { + $jwk = new JwkSet([$jwk]); + } + if (!$jwk instanceof JwkSet) { + throw new \InvalidArgumentException('JWK has to be provided'); + } + $this->jwkSet = $jwk; + foreach ($this->jwkSet->getKeys() as $jwk) { + $this->validateJwk($jwk); + } + $this->contentAlgo = $contentEncryptionAlgo; + } + + /** + * @inheritDoc + */ + public function getAlgorithmName(): string + { + if (count($this->jwkSet->getKeys()) > 1) { + return 'jwe-json-serialization'; + } else { + return $this->jwkSet->getKeys()[0]->getAlgorithm(); + } + } + + /** + * @inheritDoc + */ + public function getContentEncryptionAlgorithm(): string + { + return $this->contentAlgo; + } + + /** + * JWK Set. + * + * @return JwkSet + */ + public function getJwkSet(): JwkSet + { + return $this->jwkSet; + } + + /** + * Validate JWK values. + * + * @param Jwk $jwk + * @return void + */ + private function validateJwk(Jwk $jwk): void + { + if ($jwk->getPublicKeyUse() === Jwk::PUBLIC_KEY_USE_SIGNATURE) { + throw new EncryptionException('JWK is not meant for JWEs'); + } + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionSettingsInterface.php b/lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionSettingsInterface.php new file mode 100644 index 0000000000000..06ef79db13b37 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/JweEncryptionSettingsInterface.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jwe; + +use Magento\Framework\Jwt\EncryptionSettingsInterface; + +/** + * JWE Encryption settings. + */ +interface JweEncryptionSettingsInterface extends EncryptionSettingsInterface +{ + public const CONTENT_ENCRYPTION_ALGO_A128_HS256 = 'A128CBC-HS256'; + + public const CONTENT_ENCRYPTION_ALGO_A192_HS384 = 'A192CBC-HS384'; + + public const CONTENT_ENCRYPTION_ALGO_A256_HS512 = 'A256CBC-HS512'; + + public const CONTENT_ENCRYPTION_ALGO_A128GCM = 'A128GCM'; + + public const CONTENT_ENCRYPTION_ALGO_A192GCM = 'A192GCM'; + + public const CONTENT_ENCRYPTION_ALGO_A256GCM = 'A256GCM'; + + /** + * Algorithm used to encrypt payload. + * + * "enc" header value. + * + * @return string + */ + public function getContentEncryptionAlgorithm(): string; +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/JweHeader.php b/lib/internal/Magento/Framework/Jwt/Jwe/JweHeader.php new file mode 100644 index 0000000000000..f7aec5c38f009 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jwe/JweHeader.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jwe; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\HeaderParameterInterface; + +class JweHeader implements HeaderInterface +{ + /** + * @var JweHeaderParameterInterface[] + */ + private $parameters; + + /** + * @param JweHeaderParameterInterface[] $parameters + */ + public function __construct(array $parameters) + { + $this->parameters = []; + foreach ($parameters as $parameter) { + if (!$parameter instanceof JweHeaderParameterInterface) { + throw new \InvalidArgumentException( + sprintf('Header "%s" is not applicable to JWE tokens', $parameter->getName()) + ); + } + $this->parameters[$parameter->getName()] = $parameter; + } + } + + /** + * @inheritDoc + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * @inheritDoc + */ + public function getParameter(string $name): ?HeaderParameterInterface + { + return !empty($this->parameters[$name]) ? $this->parameters[$name] : null; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jwe/JweInterface.php b/lib/internal/Magento/Framework/Jwt/Jwe/JweInterface.php index c611d62d9ecca..d38cb878609bf 100644 --- a/lib/internal/Magento/Framework/Jwt/Jwe/JweInterface.php +++ b/lib/internal/Magento/Framework/Jwt/Jwe/JweInterface.php @@ -37,5 +37,5 @@ public function getSharedUnprotectedHeader(): ?HeaderInterface; * * @return HeaderInterface[] */ - public function getPerRecipientUnprotectedHeaders(): array; + public function getPerRecipientUnprotectedHeaders(): ?array; } diff --git a/lib/internal/Magento/Framework/Jwt/Jwk.php b/lib/internal/Magento/Framework/Jwt/Jwk.php index 692413f9a4993..ff8e66c60c659 100644 --- a/lib/internal/Magento/Framework/Jwt/Jwk.php +++ b/lib/internal/Magento/Framework/Jwt/Jwk.php @@ -67,6 +67,38 @@ class Jwk public const ALGORITHM_PS512 = 'PS512'; + public const ALGORITHM_RSA_OAEP = 'RSA-OAEP'; + + public const ALGORITHM_RSA_OAEP_256 = 'RSA-OAEP-256'; + + public const ALGORITHM_A128KW = 'A128KW'; + + public const ALGORITHM_A192KW = 'A192KW'; + + public const ALGORITHM_A256KW = 'A256KW'; + + public const ALGORITHM_DIR = 'dir'; + + public const ALGORITHM_ECDH_ES = 'ECDH-ES'; + + public const ALGORITHM_ECDH_ES_A128KW = 'ECDH-ES+A128KW'; + + public const ALGORITHM_ECDH_ES_A192KW = 'ECDH-ES+A192KW'; + + public const ALGORITHM_ECDH_ES_A256KW = 'ECDH-ES+A256KW'; + + public const ALGORITHM_A128GCMKW = 'A128GCMKW'; + + public const ALGORITHM_A192GCMKW = 'A192GCMKW'; + + public const ALGORITHM_A256GCMKW = 'A256GCMKW'; + + public const ALGORITHM_PBES2_HS256_A128KW = 'PBES2-HS256+A128KW'; + + public const ALGORITHM_PBES2_HS384_A192KW = 'PBES2-HS384+A192KW'; + + public const ALGORITHM_PBES2_HS512_A256KW = 'PBES2-HS512+A256KW'; + /** * @var string */ diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index ba02a9d61472d..c34d197bef6ab 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -71,7 +71,7 @@ public function createFromData(array $data): Jwk */ public function createHs256(string $key): Jwk { - return $this->createHmac(256, $key); + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS256); } /** @@ -82,7 +82,7 @@ public function createHs256(string $key): Jwk */ public function createHs384(string $key): Jwk { - return $this->createHmac(384, $key); + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS384); } /** @@ -93,7 +93,7 @@ public function createHs384(string $key): Jwk */ public function createHs512(string $key): Jwk { - return $this->createHmac(512, $key); + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS512); } /** @@ -303,7 +303,18 @@ public function createVerifyPs512(string $publicKey): Jwk return $this->createVerifyPs(512, $publicKey); } - private function createHmac(int $bits, string $key): Jwk + /** + * Create key to use with A128KW algorithm to encrypt JWE. + * + * @param string $key + * @return Jwk + */ + public function createA128KW(string $key): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A128KW); + } + + private function createOct(string $key, string $use, string $algo): Jwk { if (strlen($key) < 128) { throw new \InvalidArgumentException('Shared secret key must be at least 128 bits.'); @@ -312,9 +323,9 @@ private function createHmac(int $bits, string $key): Jwk return new Jwk( Jwk::KEY_TYPE_OCTET, ['k' => self::base64Encode($key)], - Jwk::PUBLIC_KEY_USE_SIGNATURE, + $use, null, - 'HS' .$bits + $algo ); } diff --git a/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureJwks.php b/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureJwks.php index 5aedcaadcfd06..03b505c6fb824 100644 --- a/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureJwks.php +++ b/lib/internal/Magento/Framework/Jwt/Jws/JwsSignatureJwks.php @@ -70,7 +70,7 @@ public function getJwkSet(): JwkSet private function validateJwk(Jwk $jwk): void { if ($jwk->getPublicKeyUse() === Jwk::PUBLIC_KEY_USE_ENCRYPTION) { - throw new EncryptionException('JWK is meant for JWEs'); + throw new EncryptionException('JWK is not meant for JWSs'); } } } From 7708daadc7c82b55b942584aef49e89309ad1165 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Wed, 3 Feb 2021 14:09:29 -0600 Subject: [PATCH 031/137] MC-38539: Introduce JWT wrapper --- .../JwtFrameworkAdapter/Model/JweManager.php | 9 +- .../JwtFrameworkAdapter/Model/JwtManager.php | 4 + .../Magento/Framework/Jwt/JwtManagerTest.php | 217 +++++++++++- lib/internal/Magento/Framework/Jwt/Jwk.php | 30 +- .../Magento/Framework/Jwt/JwkFactory.php | 330 ++++++++++++++---- 5 files changed, 505 insertions(+), 85 deletions(-) diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php index be520953a9138..26a0be36ca172 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php @@ -77,9 +77,7 @@ public function build(JweInterface $jwe, EncryptionSettingsInterface $encryption $builder = $builder->withPayload($payload->getContent()); $sharedProtected = $this->extractHeaderData($jwe->getProtectedHeader()); - if (!$jwe->getPerRecipientUnprotectedHeaders()) { - $sharedProtected['enc'] = $encryptionSettings->getContentEncryptionAlgorithm(); - } + $sharedProtected['enc'] = $encryptionSettings->getContentEncryptionAlgorithm(); if (!$jwe->getPerRecipientUnprotectedHeaders()) { $sharedProtected['alg'] = $encryptionSettings->getAlgorithmName(); } @@ -117,7 +115,10 @@ public function build(JweInterface $jwe, EncryptionSettingsInterface $encryption } $built = $builder->build(); - if ($jwe->getPerRecipientUnprotectedHeaders() && count($jwe->getPerRecipientUnprotectedHeaders()) === 1) { + if ($jwe->getPerRecipientUnprotectedHeaders() + && count($jwe->getPerRecipientUnprotectedHeaders()) === 1 + || (!$jwe->getPerRecipientUnprotectedHeaders() && $jwe->getSharedUnprotectedHeader()) + ) { return $this->serializer->serialize('jwe_json_flattened', $built); } if ($jwe->getPerRecipientUnprotectedHeaders()) { diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index d1db17222e318..8c47649e16e2e 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -11,6 +11,7 @@ use Magento\Framework\Jwt\EncryptionSettingsInterface; use Magento\Framework\Jwt\Exception\JwtException; use Magento\Framework\Jwt\Exception\MalformedTokenException; +use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface; use Magento\Framework\Jwt\Jwe\JweInterface; use Magento\Framework\Jwt\Jwk; use Magento\Framework\Jwt\Jws\JwsInterface; @@ -145,6 +146,9 @@ private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): if ($encryptionSettings instanceof JwsSignatureSettingsInterface) { return self::JWT_TYPE_JWS; } + if ($encryptionSettings instanceof JweEncryptionSettingsInterface) { + return self::JWT_TYPE_JWE; + } if ($encryptionSettings->getAlgorithmName() === Jwk::ALGORITHM_NONE) { return self::JWT_TYPE_UNSECURED; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 9895318ba91c2..8a3e89f7c1128 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -15,6 +15,7 @@ use Magento\Framework\Jwt\Claim\PrivateClaim; use Magento\Framework\Jwt\Claim\Subject; use Magento\Framework\Jwt\Header\Critical; +use Magento\Framework\Jwt\Header\KeyId; use Magento\Framework\Jwt\Header\PrivateHeaderParameter; use Magento\Framework\Jwt\Header\PublicHeaderParameter; use Magento\Framework\Jwt\Jwe\Jwe; @@ -226,11 +227,106 @@ public function getTokenVariants(): array ] ) ); + $jsonFlatSharedHeaderJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + new JweHeader( + [ + new PrivateHeaderParameter('mage', 'test') + ] + ), + null, + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); + $jsonFlatJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + null, + [ + new JweHeader( + [ + new PrivateHeaderParameter('mage', 'test') + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); + $jsonJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + new JweHeader( + [ + new PrivateHeaderParameter('mage', 'test') + ] + ), + [ + new JweHeader([new PrivateHeaderParameter('tst', 2)]), + new JweHeader([new PrivateHeaderParameter('test2', 3)]) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); + $jsonJweKids = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + ] + ), + null, + [ + new JweHeader([new PrivateHeaderParameter('tst', 2), new KeyId('1')]), + new JweHeader([new PrivateHeaderParameter('test2', 3), new KeyId('2')]) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); //Keys [$rsaPrivate, $rsaPublic] = $this->createRsaKeys(); $ecKeys = $this->createEcKeys(); - $sharedSecret = random_bytes(128); + $sharedSecret = random_bytes(2048); return [ 'jws-HS256' => [ @@ -263,7 +359,7 @@ public function getTokenVariants(): array new JwsSignatureJwks($jwkFactory->createSignRs512($rsaPrivate, 'pass')), [new JwsSignatureJwks($jwkFactory->createVerifyRs512($rsaPublic))] ], - 'jws-compact-multiple-signatures' => [ + 'jws-json-multiple-signatures' => [ $compactJws, new JwsSignatureJwks( new JwkSet( @@ -281,7 +377,7 @@ public function getTokenVariants(): array ) ] ], - 'jws-compact-multiple-signatures-one-read' => [ + 'jws-json-multiple-signatures-one-read' => [ $compactJws, new JwsSignatureJwks( new JwkSet( @@ -335,6 +431,121 @@ public function getTokenVariants(): array JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 ) ] + ], + 'jwe-A192KW' => [ + $jsonFlatSharedHeaderJwe, + new JweEncryptionJwks( + $jwkFactory->createA192KW($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createA192KW($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-A256KW' => [ + $jsonFlatJwe, + new JweEncryptionJwks( + $jwkFactory->createA256KW($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createA256KW($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-multiple-recipients' => [ + $jsonJwe, + new JweEncryptionJwks( + new JwkSet( + [ + $jwkFactory->createA256KW($sharedSecret), + $jwkFactory->createA128KW($sharedSecret) + ] + ), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + new JwkSet( + [ + $jwkFactory->createA256KW($sharedSecret), + ] + ), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-rsa-oaep' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createEncryptRsaOaep($rsaPublic), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDecryptRsaOaep($rsaPrivate, 'pass'), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-rsa-oaep-256' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createEncryptRsaOaep256($rsaPublic), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A192GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDecryptRsaOaep256($rsaPrivate, 'pass'), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A192GCM + ) + ] + ], + 'jwe-dir' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createDir( + $sharedSecret, + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A192_HS384 + ), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A192_HS384 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDir( + $sharedSecret, + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A192_HS384 + ), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A192_HS384 + ) + ] + ], + 'jwe-multiple-recipients-kids' => [ + $jsonJweKids, + new JweEncryptionJwks( + new JwkSet( + [ + $jwkFactory->createEncryptRsaOaep256($rsaPublic, '2'), + $jwkFactory->createA256KW($sharedSecret, '1') + ] + ), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + new JwkSet( + [ + $jwkFactory->createDecryptRsaOaep256($rsaPrivate, 'pass', '2') + ] + ), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] ] ]; } diff --git a/lib/internal/Magento/Framework/Jwt/Jwk.php b/lib/internal/Magento/Framework/Jwt/Jwk.php index ff8e66c60c659..b7a00319b1485 100644 --- a/lib/internal/Magento/Framework/Jwt/Jwk.php +++ b/lib/internal/Magento/Framework/Jwt/Jwk.php @@ -144,13 +144,17 @@ class Jwk */ private $x5ts256; + /** + * @var string|null + */ + private $contentEncryption; + /** * @var array */ private $data; /** - * Jwk constructor. * @param string $kty * @param array $data * @param string|null $use @@ -161,6 +165,7 @@ class Jwk * @param string|null $x5t * @param string|null $x5ts256 * @param string|null $kid + * @param string|null $contentEncryption */ public function __construct( string $kty, @@ -172,7 +177,8 @@ public function __construct( ?array $x5c = null, ?string $x5t = null, ?string $x5ts256 = null, - ?string $kid = null + ?string $kid = null, + ?string $contentEncryption = null ) { $this->kty = $kty; $this->data = $data; @@ -184,6 +190,12 @@ public function __construct( $this->x5t = $x5t; $this->x5ts256 = $x5ts256; $this->kid = $kid; + if ($contentEncryption && $alg !== self::ALGORITHM_DIR) { + throw new \InvalidArgumentException( + 'Can only specify content encryption algorithm as "alg" for JWEs with "dir" algorithm' + ); + } + $this->contentEncryption = $contentEncryption; } /** @@ -276,6 +288,16 @@ public function getKeyId(): ?string return $this->kid; } + /** + * Content encryption algorithm for JWEs with "alg" == "dir". + * + * @return string|null + */ + public function getContentEncryption(): ?string + { + return $this->contentEncryption; + } + /** * Map with algorithm (type) specific data. * @@ -297,14 +319,14 @@ public function getJsonData(): array 'kty' => $this->getKeyType(), 'use' => $this->getPublicKeyUse(), 'key_ops' => $this->getKeyOperations(), - 'alg' => $this->getAlgorithm(), + 'alg' => $this->getContentEncryption() ?? $this->getAlgorithm(), 'x5u' => $this->getX509Url(), 'x5c' => $this->getX509CertificateChain(), 'x5t' => $this->getX509Sha1Thumbprint(), 'x5t#S256' => $this->getX509Sha256Thumbprint(), 'kid' => $this->getKeyId() ]; - $data = array_merge($data, $this->getAlgoData()); + $data = array_merge($this->getAlgoData(), $data); return array_filter($data, function ($value) { return $value !== null; diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index c34d197bef6ab..e6c621e59622c 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -67,33 +67,36 @@ public function createFromData(array $data): Jwk * Create JWK for signatures generated with HMAC and SHA256 * * @param string $key + * @param string|null $kid JWK ID. * @return Jwk */ - public function createHs256(string $key): Jwk + public function createHs256(string $key, ?string $kid = null): Jwk { - return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS256); + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS256, $kid); } /** * Create JWK for signatures generated with HMAC and SHA384 * * @param string $key + * @param string|null $kid JWK ID. * @return Jwk */ - public function createHs384(string $key): Jwk + public function createHs384(string $key, ?string $kid = null): Jwk { - return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS384); + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS384, $kid); } /** * Create JWK for signatures generated with HMAC and SHA512 * * @param string $key + * @param string|null $kid JWK ID. * @return Jwk */ - public function createHs512(string $key): Jwk + public function createHs512(string $key, ?string $kid = null): Jwk { - return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS512); + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS512, $kid); } /** @@ -101,22 +104,30 @@ public function createHs512(string $key): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignRs256(string $privateKey, ?string $passPhrase): Jwk + public function createSignRs256(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignRsa(256, $privateKey, $passPhrase); + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + Jwk::ALGORITHM_RS256, + $kid + ); } /** * Create JWK to verify JWS signed with RSASSA-PKCS1-v1_5 using SHA-256. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyRs256(string $publicKey): Jwk + public function createVerifyRs256(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyRsa(256, $publicKey); + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_RS256, $kid); } /** @@ -124,22 +135,30 @@ public function createVerifyRs256(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignRs384(string $privateKey, ?string $passPhrase): Jwk + public function createSignRs384(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignRsa(384, $privateKey, $passPhrase); + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + Jwk::ALGORITHM_RS384, + $kid + ); } /** * Create JWK to verify JWS signed with RSASSA-PKCS1-v1_5 using SHA-384. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyRs384(string $publicKey): Jwk + public function createVerifyRs384(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyRsa(384, $publicKey); + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_RS384, $kid); } /** @@ -147,22 +166,30 @@ public function createVerifyRs384(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignRs512(string $privateKey, ?string $passPhrase): Jwk + public function createSignRs512(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignRsa(512, $privateKey, $passPhrase); + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + Jwk::ALGORITHM_RS512, + $kid + ); } /** * Create JWK to verify JWS signed with RSASSA-PKCS1-v1_5 using SHA-512. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyRs512(string $publicKey): Jwk + public function createVerifyRs512(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyRsa(512, $publicKey); + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_RS512, $kid); } /** @@ -170,22 +197,24 @@ public function createVerifyRs512(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignEs256(string $privateKey, ?string $passPhrase): Jwk + public function createSignEs256(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignEs(256, $privateKey, $passPhrase); + return $this->createSignEs(256, $privateKey, $passPhrase, $kid); } /** * Create JWK to verify JWS signed with ECDSA using P-256 and SHA-256. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyEs256(string $publicKey): Jwk + public function createVerifyEs256(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyEs(256, $publicKey); + return $this->createVerifyEs(256, $publicKey, $kid); } /** @@ -193,22 +222,24 @@ public function createVerifyEs256(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignEs384(string $privateKey, ?string $passPhrase): Jwk + public function createSignEs384(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignEs(384, $privateKey, $passPhrase); + return $this->createSignEs(384, $privateKey, $passPhrase, $kid); } /** * Create JWK to verify JWS signed with ECDSA using P-384 and SHA-384 . * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyEs384(string $publicKey): Jwk + public function createVerifyEs384(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyEs(384, $publicKey); + return $this->createVerifyEs(384, $publicKey, $kid); } /** @@ -216,22 +247,24 @@ public function createVerifyEs384(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignEs512(string $privateKey, ?string $passPhrase): Jwk + public function createSignEs512(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignEs(512, $privateKey, $passPhrase); + return $this->createSignEs(512, $privateKey, $passPhrase, $kid); } /** * Create JWK to verify JWS signed with ECDSA using P-521 and SHA-512. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyEs512(string $publicKey): Jwk + public function createVerifyEs512(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyEs(512, $publicKey); + return $this->createVerifyEs(512, $publicKey, $kid); } /** @@ -239,22 +272,30 @@ public function createVerifyEs512(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignPs256(string $privateKey, ?string $passPhrase): Jwk + public function createSignPs256(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignPs(256, $privateKey, $passPhrase); + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + Jwk::ALGORITHM_PS256, + $kid + ); } /** * Create JWK to verify JWS signed with RSASSA-PSS using SHA-256 and MGF1 with SHA-256. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyPs256(string $publicKey): Jwk + public function createVerifyPs256(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyPs(256, $publicKey); + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_PS256, $kid); } /** @@ -262,22 +303,30 @@ public function createVerifyPs256(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignPs384(string $privateKey, ?string $passPhrase): Jwk + public function createSignPs384(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignPs(384, $privateKey, $passPhrase); + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + Jwk::ALGORITHM_PS384, + $kid + ); } /** * Create JWK to verify JWS signed with RSASSA-PSS using SHA-384 and MGF1 with SHA-384. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyPs384(string $publicKey): Jwk + public function createVerifyPs384(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyPs(384, $publicKey); + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_PS384, $kid); } /** @@ -285,39 +334,163 @@ public function createVerifyPs384(string $publicKey): Jwk * * @param string $privateKey * @param string|null $passPhrase + * @param string|null $kid JWK ID. * @return Jwk */ - public function createSignPs512(string $privateKey, ?string $passPhrase): Jwk + public function createSignPs512(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignPs(512, $privateKey, $passPhrase); + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + Jwk::ALGORITHM_PS512, + $kid + ); } /** * Create JWK to verify JWS signed with RSASSA-PSS using SHA-512 and MGF1 with SHA-512. * * @param string $publicKey + * @param string|null $kid JWK ID. * @return Jwk */ - public function createVerifyPs512(string $publicKey): Jwk + public function createVerifyPs512(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyPs(512, $publicKey); + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_PS512, $kid); } /** * Create key to use with A128KW algorithm to encrypt JWE. * * @param string $key + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createA128KW(string $key, ?string $kid = null): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A128KW, $kid); + } + + /** + * Create key to use with A192KW algorithm to encrypt JWE. + * + * @param string $key + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createA192KW(string $key, ?string $kid = null): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A192KW, $kid); + } + + /** + * Create key to use with A256KW algorithm to encrypt JWE. + * + * @param string $key + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createA256KW(string $key, ?string $kid = null): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A256KW, $kid); + } + + /** + * Create RSA key to use with RSA-OAEP algorithm to encrypt JWE. + * + * @param string $publicKey + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createEncryptRsaOaep(string $publicKey, ?string $kid = null): Jwk + { + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_RSA_OAEP, $kid); + } + + /** + * Create RSA key to use with RSA-OAEP algorithm to decrypt JWE. + * + * @param string $privateKey + * @param string|null $passPhrase + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createDecryptRsaOaep(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk + { + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + Jwk::ALGORITHM_RSA_OAEP, + $kid + ); + } + + /** + * Create RSA key to use with RSA-OAEP-256 algorithm to encrypt JWE. + * + * @param string $publicKey + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createEncryptRsaOaep256(string $publicKey, ?string $kid = null): Jwk + { + return $this->createPublicRsa($publicKey, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_RSA_OAEP_256, $kid); + } + + /** + * Create RSA key to use with RSA-OAEP-256 algorithm to decrypt JWE. + * + * @param string $privateKey + * @param string|null $passPhrase + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createDecryptRsaOaep256(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk + { + return $this->createPrivateRsa( + $privateKey, + $passPhrase, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + Jwk::ALGORITHM_RSA_OAEP_256, + $kid + ); + } + + /** + * Create JWK to use with "dir" algorithm for JWEs. + * + * @param string $key + * @param string $contentEncryptionAlgorithm + * @param string|null $kid JWK ID. * @return Jwk */ - public function createA128KW(string $key): Jwk + public function createDir(string $key, string $contentEncryptionAlgorithm, ?string $kid = null): Jwk { - return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A128KW); + if (strlen($key) < 2048) { + throw new \InvalidArgumentException('Shared secret key must be at least 2048 bits.'); + } + + return new Jwk( + Jwk::KEY_TYPE_OCTET, + ['k' => self::base64Encode($key)], + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + null, + Jwk::ALGORITHM_DIR, + null, + null, + null, + null, + $kid, + $contentEncryptionAlgorithm + ); } - private function createOct(string $key, string $use, string $algo): Jwk + private function createOct(string $key, string $use, string $algo, ?string $kid): Jwk { - if (strlen($key) < 128) { - throw new \InvalidArgumentException('Shared secret key must be at least 128 bits.'); + if (strlen($key) < 2048) { + throw new \InvalidArgumentException('Shared secret key must be at least 2048 bits.'); } return new Jwk( @@ -325,11 +498,16 @@ private function createOct(string $key, string $use, string $algo): Jwk ['k' => self::base64Encode($key)], $use, null, - $algo + $algo, + null, + null, + null, + null, + $kid ); } - private function createSignRsa(int $bits, string $key, ?string $pass): Jwk + private function createPrivateRsa(string $key, ?string $pass, string $use, string $algorithm, ?string $kid): Jwk { $resource = openssl_get_privatekey($key, (string)$pass); $keyData = openssl_pkey_get_details($resource)['rsa']; @@ -354,13 +532,18 @@ private function createSignRsa(int $bits, string $key, ?string $pass): Jwk return new Jwk( Jwk::KEY_TYPE_RSA, $jwkData, - Jwk::PUBLIC_KEY_USE_SIGNATURE, + $use, + null, + $algorithm, + null, null, - 'RS' .$bits + null, + null, + $kid ); } - private function createVerifyRsa(int $bits, string $key): Jwk + private function createPublicRsa(string $key, string $use, string $algorithm, ?string $kid): Jwk { $resource = openssl_get_publickey($key); $keyData = openssl_pkey_get_details($resource)['rsa']; @@ -379,29 +562,18 @@ private function createVerifyRsa(int $bits, string $key): Jwk return new Jwk( Jwk::KEY_TYPE_RSA, $jwkData, - Jwk::PUBLIC_KEY_USE_SIGNATURE, + $use, + null, + $algorithm, + null, + null, null, - 'RS' .$bits + null, + $kid ); } - private function createSignPs(int $bits, string $key, ?string $pass): Jwk - { - $data = $this->createSignRsa($bits, $key, $pass)->getJsonData(); - $data['alg'] = 'PS' .$bits; - - return $this->createFromData($data); - } - - private function createVerifyPs(int $bits, string $key): Jwk - { - $data = $this->createVerifyRsa($bits, $key)->getJsonData(); - $data['alg'] = 'PS' .$bits; - - return $this->createFromData($data); - } - - private function createSignEs(int $bits, string $key, ?string $pass): Jwk + private function createSignEs(int $bits, string $key, ?string $pass, ?string $kid): Jwk { $resource = openssl_get_privatekey($key, (string)$pass); $keyData = openssl_pkey_get_details($resource)['ec']; @@ -423,11 +595,16 @@ private function createSignEs(int $bits, string $key, ?string $pass): Jwk ], Jwk::PUBLIC_KEY_USE_SIGNATURE, null, - 'ES' .$bits + 'ES' .$bits, + null, + null, + null, + null, + $kid ); } - private function createVerifyEs(int $bits, string $key): Jwk + private function createVerifyEs(int $bits, string $key, ?string $kid): Jwk { $resource = openssl_get_publickey($key); $keyData = openssl_pkey_get_details($resource)['ec']; @@ -448,7 +625,12 @@ private function createVerifyEs(int $bits, string $key): Jwk ], Jwk::PUBLIC_KEY_USE_SIGNATURE, null, - 'ES' .$bits + 'ES' .$bits, + null, + null, + null, + null, + $kid ); } From 2d9c0c6284cbb29a29fa10dff476970db06c3ee0 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Thu, 4 Feb 2021 09:37:19 -0600 Subject: [PATCH 032/137] MC-38539: Introduce JWT wrapper --- .../JwtFrameworkAdapter/Model/JweManager.php | 3 + .../Magento/Framework/Jwt/JwtManagerTest.php | 130 ++++++++ .../Magento/Framework/Jwt/JwkFactory.php | 314 +++++++++++++++++- 3 files changed, 429 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php index 26a0be36ca172..805785ba17989 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php @@ -78,6 +78,9 @@ public function build(JweInterface $jwe, EncryptionSettingsInterface $encryption $sharedProtected = $this->extractHeaderData($jwe->getProtectedHeader()); $sharedProtected['enc'] = $encryptionSettings->getContentEncryptionAlgorithm(); + if ($payload->getContentType()) { + $sharedProtected['cty'] = $payload->getContentType(); + } if (!$jwe->getPerRecipientUnprotectedHeaders()) { $sharedProtected['alg'] = $encryptionSettings->getAlgorithmName(); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 8a3e89f7c1128..564ce9cffe3bd 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -546,6 +546,136 @@ public function getTokenVariants(): array JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 ) ] + ], + 'jwe-ECDH-ES-with-EC' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createEncryptEcdhEsWithEc($ecKeys[256][1]), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDecryptEcdhEsWithEc($ecKeys[256][0], 'pass'), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-ECDH-ES-A128-with-EC' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createEncryptEcdhEsA128kwWithEc($ecKeys[256][1]), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDecryptEcdhEsA128kwWithEc($ecKeys[256][0], 'pass'), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-ECDH-ES-A192-with-EC' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createEncryptEcdhEsA192kwWithEc($ecKeys[256][1]), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDecryptEcdhEsA192kwWithEc($ecKeys[256][0], 'pass'), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-ECDH-ES-A256-with-EC' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createEncryptEcdhEsA256kwWithEc($ecKeys[256][1]), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ), + [ + new JweEncryptionJwks( + $jwkFactory->createDecryptEcdhEsA256kwWithEc($ecKeys[256][0], 'pass'), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128_HS256 + ) + ] + ], + 'jwe-A128GCMKW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createA128Gcmkw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createA128Gcmkw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ) + ] + ], + 'jwe-A192GCMKW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createA192Gcmkw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createA192Gcmkw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ) + ] + ], + 'jwe-A256GCMKW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createA256Gcmkw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createA256Gcmkw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ) + ] + ], + 'jwe-PBES2-HS256+A128KW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createPbes2Hs256A128kw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createPbes2Hs256A128kw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ) + ] + ], + 'jwe-PBES2-HS384+A192KW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createPbes2Hs384A192kw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createPbes2Hs384A192kw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ) + ] + ], + 'jwe-PBES2-HS512+A256KW' => [ + $flatJwe, + new JweEncryptionJwks( + $jwkFactory->createPbes2Hs512A256kw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ), + [ + new JweEncryptionJwks( + $jwkFactory->createPbes2Hs512A256kw($sharedSecret), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ) + ] ] ]; } diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index e6c621e59622c..531126e88b9e2 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -202,7 +202,14 @@ public function createVerifyRs512(string $publicKey, ?string $kid = null): Jwk */ public function createSignEs256(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignEs(256, $privateKey, $passPhrase, $kid); + return $this->createPrivateEc( + $privateKey, + $passPhrase, + 256, + Jwk::ALGORITHM_ES256, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + $kid + ); } /** @@ -214,7 +221,13 @@ public function createSignEs256(string $privateKey, ?string $passPhrase, ?string */ public function createVerifyEs256(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyEs(256, $publicKey, $kid); + return $this->createPublicEc( + $publicKey, + 256, + Jwk::ALGORITHM_ES256, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + $kid + ); } /** @@ -227,7 +240,14 @@ public function createVerifyEs256(string $publicKey, ?string $kid = null): Jwk */ public function createSignEs384(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignEs(384, $privateKey, $passPhrase, $kid); + return $this->createPrivateEc( + $privateKey, + $passPhrase, + 384, + Jwk::ALGORITHM_ES384, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + $kid + ); } /** @@ -239,7 +259,13 @@ public function createSignEs384(string $privateKey, ?string $passPhrase, ?string */ public function createVerifyEs384(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyEs(384, $publicKey, $kid); + return $this->createPublicEc( + $publicKey, + 384, + Jwk::ALGORITHM_ES384, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + $kid + ); } /** @@ -252,7 +278,14 @@ public function createVerifyEs384(string $publicKey, ?string $kid = null): Jwk */ public function createSignEs512(string $privateKey, ?string $passPhrase, ?string $kid = null): Jwk { - return $this->createSignEs(512, $privateKey, $passPhrase, $kid); + return $this->createPrivateEc( + $privateKey, + $passPhrase, + 512, + Jwk::ALGORITHM_ES512, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + $kid + ); } /** @@ -264,7 +297,13 @@ public function createSignEs512(string $privateKey, ?string $passPhrase, ?string */ public function createVerifyEs512(string $publicKey, ?string $kid = null): Jwk { - return $this->createVerifyEs(512, $publicKey, $kid); + return $this->createPublicEc( + $publicKey, + 512, + Jwk::ALGORITHM_ES512, + Jwk::PUBLIC_KEY_USE_SIGNATURE, + $kid + ); } /** @@ -487,6 +526,230 @@ public function createDir(string $key, string $contentEncryptionAlgorithm, ?stri ); } + /** + * Create JWK to use ECDH-ES algorithm with EC key to encrypt JWE. + * + * @param string $publicEcKey + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createEncryptEcdhEsWithEc(string $publicEcKey, ?string $kid = null): Jwk + { + return $this->createPublicEc( + $publicEcKey, + null, + Jwk::ALGORITHM_ECDH_ES, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES algorithm with EC key to decrypt JWE. + * + * @param string $privateEcKey + * @param string|null $passPhrase + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createDecryptEcdhEsWithEc(string $privateEcKey, ?string $passPhrase, ?string $kid = null): Jwk + { + return $this->createPrivateEc( + $privateEcKey, + $passPhrase, + null, + Jwk::ALGORITHM_ECDH_ES, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES+A128KW algorithm with EC key to encrypt JWE. + * + * @param string $publicEcKey + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createEncryptEcdhEsA128kwWithEc(string $publicEcKey, ?string $kid = null): Jwk + { + return $this->createPublicEc( + $publicEcKey, + null, + Jwk::ALGORITHM_ECDH_ES_A128KW, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES+A128KW algorithm with EC key to decrypt JWE. + * + * @param string $privateEcKey + * @param string|null $passPhrase + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createDecryptEcdhEsA128kwWithEc(string $privateEcKey, ?string $passPhrase, ?string $kid = null): Jwk + { + return $this->createPrivateEc( + $privateEcKey, + $passPhrase, + null, + Jwk::ALGORITHM_ECDH_ES_A128KW, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES+A192KW algorithm with EC key to encrypt JWE. + * + * @param string $publicEcKey + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createEncryptEcdhEsA192kwWithEc(string $publicEcKey, ?string $kid = null): Jwk + { + return $this->createPublicEc( + $publicEcKey, + null, + Jwk::ALGORITHM_ECDH_ES_A192KW, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES+A192KW algorithm with EC key to decrypt JWE. + * + * @param string $privateEcKey + * @param string|null $passPhrase + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createDecryptEcdhEsA192kwWithEc(string $privateEcKey, ?string $passPhrase, ?string $kid = null): Jwk + { + return $this->createPrivateEc( + $privateEcKey, + $passPhrase, + null, + Jwk::ALGORITHM_ECDH_ES_A192KW, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES+A256KW algorithm with EC key to encrypt JWE. + * + * @param string $publicEcKey + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createEncryptEcdhEsA256kwWithEc(string $publicEcKey, ?string $kid = null): Jwk + { + return $this->createPublicEc( + $publicEcKey, + null, + Jwk::ALGORITHM_ECDH_ES_A256KW, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to use ECDH-ES+A256KW algorithm with EC key to decrypt JWE. + * + * @param string $privateEcKey + * @param string|null $passPhrase + * @param string|null $kid JWK ID. + * @return Jwk + */ + public function createDecryptEcdhEsA256kwWithEc(string $privateEcKey, ?string $passPhrase, ?string $kid = null): Jwk + { + return $this->createPrivateEc( + $privateEcKey, + $passPhrase, + null, + Jwk::ALGORITHM_ECDH_ES_A256KW, + Jwk::PUBLIC_KEY_USE_ENCRYPTION, + $kid + ); + } + + /** + * Create JWK to encrypt/decrypt JWEs with A128GCMKW algorithm. + * + * @param string $key + * @param string|null $kid + * @return Jwk + */ + public function createA128Gcmkw(string $key, ?string $kid = null): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A128GCMKW, $kid); + } + + /** + * Create JWK to encrypt/decrypt JWEs with A192GCMKW algorithm. + * + * @param string $key + * @param string|null $kid + * @return Jwk + */ + public function createA192Gcmkw(string $key, ?string $kid = null): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A192GCMKW, $kid); + } + + /** + * Create JWK to encrypt/decrypt JWEs with A256GCMKW algorithm. + * + * @param string $key + * @param string|null $kid + * @return Jwk + */ + public function createA256Gcmkw(string $key, ?string $kid = null): Jwk + { + return $this->createOct($key, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_A256GCMKW, $kid); + } + + /** + * Create JWK to encrypt/decrypt JWEs with PBES2-HS256+A128KW algorithm. + * + * @param string $password + * @param string|null $kid + * @return Jwk + */ + public function createPbes2Hs256A128kw(string $password, ?string $kid = null): Jwk + { + return $this->createOct($password, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_PBES2_HS256_A128KW, $kid); + } + + /** + * Create JWK to encrypt/decrypt JWEs with PBES2-HS384+A192KW algorithm. + * + * @param string $password + * @param string|null $kid + * @return Jwk + */ + public function createPbes2Hs384A192kw(string $password, ?string $kid = null): Jwk + { + return $this->createOct($password, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_PBES2_HS384_A192KW, $kid); + } + + /** + * Create JWK to encrypt/decrypt JWEs with PBES2-HS512+A256KW algorithm. + * + * @param string $password + * @param string|null $kid + * @return Jwk + */ + public function createPbes2Hs512A256kw(string $password, ?string $kid = null): Jwk + { + return $this->createOct($password, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_PBES2_HS512_A256KW, $kid); + } + private function createOct(string $key, string $use, string $algo, ?string $kid): Jwk { if (strlen($key) < 2048) { @@ -573,16 +836,24 @@ private function createPublicRsa(string $key, string $use, string $algorithm, ?s ); } - private function createSignEs(int $bits, string $key, ?string $pass, ?string $kid): Jwk - { + private function createPrivateEc( + string $key, + ?string $pass, + ?int $validateCurveBits, + string $algorithm, + string $use, + ?string $kid + ): Jwk { $resource = openssl_get_privatekey($key, (string)$pass); $keyData = openssl_pkey_get_details($resource)['ec']; openssl_free_key($resource); if (!array_key_exists($keyData['curve_oid'], self::EC_CURVE_MAP)) { throw new \RuntimeException('Unsupported EC curve'); } - if ($bits !== self::EC_CURVE_MAP[$keyData['curve_oid']]['bits']) { - throw new \RuntimeException('The key cannot be used with SHA-' .$bits .' hashing algorithm'); + if ($validateCurveBits && $validateCurveBits !== self::EC_CURVE_MAP[$keyData['curve_oid']]['bits']) { + throw new \RuntimeException( + 'The key cannot be used with SHA-' .$validateCurveBits .' hashing algorithm' + ); } return new Jwk( @@ -593,9 +864,9 @@ private function createSignEs(int $bits, string $key, ?string $pass, ?string $ki 'y' => self::base64Encode($keyData['y']), 'crv' => self::EC_CURVE_MAP[$keyData['curve_oid']]['name'] ], - Jwk::PUBLIC_KEY_USE_SIGNATURE, + $use, null, - 'ES' .$bits, + $algorithm, null, null, null, @@ -604,16 +875,23 @@ private function createSignEs(int $bits, string $key, ?string $pass, ?string $ki ); } - private function createVerifyEs(int $bits, string $key, ?string $kid): Jwk - { + private function createPublicEc( + string $key, + ?int $validateCurveBits, + string $algorithm, + string $use, + ?string $kid + ): Jwk { $resource = openssl_get_publickey($key); $keyData = openssl_pkey_get_details($resource)['ec']; openssl_free_key($resource); if (!array_key_exists($keyData['curve_oid'], self::EC_CURVE_MAP)) { throw new \RuntimeException('Unsupported EC curve'); } - if ($bits !== self::EC_CURVE_MAP[$keyData['curve_oid']]['bits']) { - throw new \RuntimeException('The key cannot be used with SHA-' .$bits .' hashing algorithm'); + if ($validateCurveBits && $validateCurveBits !== self::EC_CURVE_MAP[$keyData['curve_oid']]['bits']) { + throw new \RuntimeException( + 'The key cannot be used with SHA-' .$validateCurveBits .' hashing algorithm' + ); } return new Jwk( @@ -623,9 +901,9 @@ private function createVerifyEs(int $bits, string $key, ?string $kid): Jwk 'y' => self::base64Encode($keyData['y']), 'crv' => self::EC_CURVE_MAP[$keyData['curve_oid']]['name'] ], - Jwk::PUBLIC_KEY_USE_SIGNATURE, + $use, null, - 'ES' .$bits, + $algorithm, null, null, null, From 0c9035bac2ff4a47cb358e7eaea29ee745e3db0c Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Thu, 4 Feb 2021 10:23:17 -0600 Subject: [PATCH 033/137] MC-38539: Introduce JWT wrapper --- .../Model/JwsAlgorithmManagerFactory.php | 3 +- .../JwtFrameworkAdapter/Model/JwtManager.php | 23 ++- .../Model/UnsecuredJwtFactory.php | 79 +++++++++ .../Model/UnsecuredJwtManager.php | 164 ++++++++++++++++++ .../Magento/Framework/Jwt/JwtManagerTest.php | 35 ++++ .../Magento/Framework/Jwt/JwkFactory.php | 7 + .../Framework/Jwt/Unsecured/UnsecuredJwt.php | 84 +++++++++ .../Jwt/Unsecured/UnsecuredJwtInterface.php | 15 ++ 8 files changed, 408 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtFactory.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtManager.php create mode 100644 lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php index 1c0a90ffe862e..a4a5c702b5864 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php @@ -27,7 +27,8 @@ class JwsAlgorithmManagerFactory \Jose\Component\Signature\Algorithm\ES256::class, \Jose\Component\Signature\Algorithm\ES384::class, \Jose\Component\Signature\Algorithm\ES512::class, - \Jose\Component\Signature\Algorithm\EdDSA::class + \Jose\Component\Signature\Algorithm\EdDSA::class, + \Jose\Component\Signature\Algorithm\None::class ]; public function create(): AlgorithmManager diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index 8c47649e16e2e..fb23dd36bd8e7 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -9,6 +9,7 @@ namespace Magento\JwtFrameworkAdapter\Model; use Magento\Framework\Jwt\EncryptionSettingsInterface; +use Magento\Framework\Jwt\Exception\EncryptionException; use Magento\Framework\Jwt\Exception\JwtException; use Magento\Framework\Jwt\Exception\MalformedTokenException; use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface; @@ -18,6 +19,7 @@ use Magento\Framework\Jwt\Jws\JwsSignatureSettingsInterface; use Magento\Framework\Jwt\JwtInterface; use Magento\Framework\Jwt\JwtManagerInterface; +use Magento\Framework\Jwt\Unsecured\NoEncryption; use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; /** @@ -75,14 +77,20 @@ class JwtManager implements JwtManagerInterface */ private $jweManager; + /** + * @var UnsecuredJwtManager + */ + private $unsecuredManager; + /** * @param JwsManager $jwsManager * @param JweManager $jweManager */ - public function __construct(JwsManager $jwsManager, JweManager $jweManager) + public function __construct(JwsManager $jwsManager, JweManager $jweManager, UnsecuredJwtManager $unsecuredManager) { $this->jwsManager = $jwsManager; $this->jweManager = $jweManager; + $this->unsecuredManager = $unsecuredManager; } /** @@ -100,6 +108,13 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio if ($jwt instanceof JweInterface) { return $this->jweManager->build($jwt, $encryption); } + if ($jwt instanceof UnsecuredJwtInterface) { + if (!$encryption instanceof NoEncryption) { + throw new EncryptionException('Unsecured JWTs can only work with no encryption settings'); + } + + return $this->unsecuredManager->build($jwt); + } } catch (\Throwable $exception) { if (!$exception instanceof JwtException) { $exception = new JwtException('Failed to generate a JWT', 0, $exception); @@ -126,6 +141,9 @@ public function read(string $token, array $acceptableEncryption): JwtInterface case self::JWT_TYPE_JWE: $read = $this->jweManager->read($token, $encryptionSettings); break; + case self::JWT_TYPE_UNSECURED: + $read = $this->unsecuredManager->read($token); + break; } } catch (\Throwable $exception) { if (!$exception instanceof JwtException) { @@ -149,6 +167,9 @@ private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): if ($encryptionSettings instanceof JweEncryptionSettingsInterface) { return self::JWT_TYPE_JWE; } + if ($encryptionSettings instanceof NoEncryption) { + return self::JWT_TYPE_UNSECURED; + } if ($encryptionSettings->getAlgorithmName() === Jwk::ALGORITHM_NONE) { return self::JWT_TYPE_UNSECURED; diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtFactory.php new file mode 100644 index 0000000000000..2d9fa891b9103 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtFactory.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Magento\Framework\Jwt\Jws\Jws; +use Magento\Framework\Jwt\Jws\JwsHeader; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayload; +use Magento\Framework\Jwt\Payload\NestedPayload; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\Framework\Jwt\Unsecured\UnsecuredJwt; +use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; +use Magento\JwtFrameworkAdapter\Model\Data\Claim; +use Magento\JwtFrameworkAdapter\Model\Data\Header; + +/** + * Creates unsecure JWT DTOs. + */ +class UnsecuredJwtFactory +{ + public function create( + array $protectedHeaderMaps, + ?array $unprotectedHeaderMaps, + string $payload + ): UnsecuredJwtInterface { + $cty = null; + $protectedHeaders = []; + foreach ($protectedHeaderMaps as $protectedHeaderMap) { + $parameters = []; + foreach ($protectedHeaderMap as $header => $headerValue) { + $parameters[] = new Header($header, $headerValue, null); + if ($header === 'cty') { + $cty = $headerValue; + } + } + $protectedHeaders[] = new JwsHeader($parameters); + } + $publicHeaders = null; + if ($unprotectedHeaderMaps) { + $publicHeaders = []; + foreach ($unprotectedHeaderMaps as $unprotectedHeaderMap) { + $parameters = []; + foreach ($unprotectedHeaderMap as $header => $headerValue) { + $parameters[] = new Header($header, $headerValue, null); + if ($header === 'cty') { + $cty = $headerValue; + } + } + $publicHeaders[] = new JwsHeader($parameters); + } + } + if ($cty) { + if ($cty === NestedPayloadInterface::CONTENT_TYPE) { + $payload = new NestedPayload($payload); + } else { + $payload = new ArbitraryPayload($payload); + } + } else { + $claimData = json_decode($payload, true); + $claims = []; + foreach ($claimData as $name => $value) { + $claims[] = new Claim($name, $value, null); + } + $payload = new ClaimsPayload($claims); + } + + return new UnsecuredJwt( + $protectedHeaders, + $payload, + $publicHeaders + ); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtManager.php new file mode 100644 index 0000000000000..d3a35d5a0ad31 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/UnsecuredJwtManager.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Component\Signature\JWSBuilder; +use Jose\Component\Signature\JWSLoader; +use Jose\Component\Signature\Serializer\JWSSerializerManager; +use Magento\Framework\Jwt\Exception\JwtException; +use Magento\Framework\Jwt\Exception\MalformedTokenException; +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\Jwk; +use Jose\Component\Core\JWK as AdapterJwk; +use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; + +/** + * Works with Unsecured JWT. + */ +class UnsecuredJwtManager +{ + /** + * @var JWSBuilder + */ + private $jwsBuilder; + + /** + * @var JWSLoader + */ + private $jwsLoader; + + /** + * @var JWSSerializerManager + */ + private $jwsSerializer; + + /** + * @var UnsecuredJwtFactory + */ + private $jwtFactory; + + /** + * @param JwsBuilderFactory $builderFactory + * @param JwsSerializerPoolFactory $serializerPoolFactory + * @param JwsLoaderFactory $jwsLoaderFactory + * @param UnsecuredJwtFactory $jwtFactory + */ + public function __construct( + JwsBuilderFactory $builderFactory, + JwsSerializerPoolFactory $serializerPoolFactory, + JwsLoaderFactory $jwsLoaderFactory, + UnsecuredJwtFactory $jwtFactory + ) { + $this->jwsBuilder = $builderFactory->create(); + $this->jwsSerializer = $serializerPoolFactory->create(); + $this->jwsLoader = $jwsLoaderFactory->create(); + $this->jwtFactory = $jwtFactory; + } + + /** + * Generate unsecured JWT token. + * + * @param UnsecuredJwtInterface $jwt + * @return string + * @throws JwtException + */ + public function build(UnsecuredJwtInterface $jwt): string + { + $signaturesCount = count($jwt->getProtectedHeaders()); + if ($jwt->getUnprotectedHeaders() + && count($jwt->getUnprotectedHeaders()) !== $signaturesCount + ) { + throw new MalformedTokenException('There must be an equal number of protected and unprotected headers.'); + } + $builder = $this->jwsBuilder->create(); + $builder = $builder->withPayload($jwt->getPayload()->getContent()); + for ($i = 0; $i < $signaturesCount; $i++) { + $protected = []; + if ($jwt->getPayload()->getContentType()) { + $protected['cty'] = $jwt->getPayload()->getContentType(); + } + if ($jwt->getProtectedHeaders()) { + $protected = $this->extractHeaderData($jwt->getProtectedHeaders()[$i]); + } + $protected['alg'] = Jwk::ALGORITHM_NONE; + $unprotected = []; + if ($jwt->getUnprotectedHeaders()) { + $unprotected = $this->extractHeaderData($jwt->getUnprotectedHeaders()[$i]); + } + $builder = $builder->addSignature( + new AdapterJwk(['kty' => 'none', 'alg' => 'none']), + $protected, + $unprotected + ); + } + $jwsCreated = $builder->build(); + + if ($signaturesCount > 1) { + return $this->jwsSerializer->serialize('jws_json_general', $jwsCreated); + } + if ($jwt->getUnprotectedHeaders()) { + return $this->jwsSerializer->serialize('jws_json_flattened', $jwsCreated); + } + return $this->jwsSerializer->serialize('jws_compact', $jwsCreated); + } + + /** + * Read unsecured JWT token. + * + * @param string $token + * @return UnsecuredJwtInterface + * @throws JwtException + */ + public function read(string $token): UnsecuredJwtInterface + { + try { + $jws = $this->jwsLoader->loadAndVerifyWithKey( + $token, + new AdapterJwk(['kty' => 'none', 'alg' => 'none']), + $signature, + null + ); + } catch (\Throwable $exception) { + throw new MalformedTokenException('Failed to read JWT token', 0, $exception); + } + if ($jws->isPayloadDetached()) { + throw new JwtException('Detached payload is not supported'); + } + $protectedHeaders = []; + $publicHeaders = []; + foreach ($jws->getSignatures() as $signature) { + $protectedHeaders[] = $signature->getProtectedHeader(); + if ($signature->getHeader()) { + $publicHeaders[] = $signature->getHeader(); + } + } + + return $this->jwtFactory->create( + $protectedHeaders, + $publicHeaders, + $jws->getPayload() + ); + } + + /** + * Extract JOSE header data. + * + * @param HeaderInterface $header + * @return array + */ + private function extractHeaderData(HeaderInterface $header): array + { + $data = []; + foreach ($header->getParameters() as $parameter) { + $data[$parameter->getName()] = $parameter->getValue(); + } + + return $data; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 564ce9cffe3bd..5c3c389bbb5c7 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -30,6 +30,8 @@ use Magento\Framework\Jwt\Payload\ClaimsPayload; use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\Framework\Jwt\Unsecured\NoEncryption; +use Magento\Framework\Jwt\Unsecured\UnsecuredJwt; use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -130,6 +132,14 @@ public function testCreateRead( } if ($jwt instanceof UnsecuredJwtInterface) { $this->assertInstanceOf(UnsecuredJwtInterface::class, $recreated); + /** @var UnsecuredJwt $recreated */ + if (!$jwt->getUnprotectedHeaders()) { + $this->assertNull($recreated->getUnprotectedHeaders()); + } else { + $this->assertTrue(count($recreated->getUnprotectedHeaders()) >= 1); + $this->verifyAgainstHeaders($jwt->getUnprotectedHeaders(), $recreated->getUnprotectedHeaders()[0]); + } + $this->verifyAgainstHeaders($jwt->getProtectedHeaders(), $recreated->getProtectedHeaders()[0]); } } @@ -322,6 +332,26 @@ public function getTokenVariants(): array ] ) ); + $flatUnsecured = new UnsecuredJwt( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ), + null + ); //Keys [$rsaPrivate, $rsaPublic] = $this->createRsaKeys(); @@ -676,6 +706,11 @@ public function getTokenVariants(): array JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM ) ] + ], + 'unsecured-jwt' => [ + $flatUnsecured, + new NoEncryption(), + [new NoEncryption()] ] ]; } diff --git a/lib/internal/Magento/Framework/Jwt/JwkFactory.php b/lib/internal/Magento/Framework/Jwt/JwkFactory.php index 531126e88b9e2..14561b5ed98f2 100644 --- a/lib/internal/Magento/Framework/Jwt/JwkFactory.php +++ b/lib/internal/Magento/Framework/Jwt/JwkFactory.php @@ -750,6 +750,13 @@ public function createPbes2Hs512A256kw(string $password, ?string $kid = null): J return $this->createOct($password, Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_PBES2_HS512_A256KW, $kid); } + public function createNone(): Jwk + { + return new Jwk( + + ); + } + private function createOct(string $key, string $use, string $algo, ?string $kid): Jwk { if (strlen($key) < 2048) { diff --git a/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php b/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php new file mode 100644 index 0000000000000..47523a839cba7 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Unsecured; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\PayloadInterface; + +class UnsecuredJwt implements UnsecuredJwtInterface +{ + /** + * @var HeaderInterface[] + */ + private $protectedHeaders; + + /** + * @var HeaderInterface[]|null + */ + private $unprotectedHeaders; + + /** + * @var PayloadInterface + */ + private $payload; + + /** + * @param HeaderInterface[] $protectedHeaders + * @param PayloadInterface $payload + * @param HeaderInterface[]|null $unprotectedHeaders + */ + public function __construct(array $protectedHeaders, PayloadInterface $payload, ?array $unprotectedHeaders) + { + if (!$protectedHeaders) { + throw new \InvalidArgumentException('Need at least 1 header'); + } + $this->protectedHeaders = array_values($protectedHeaders); + $this->payload = $payload; + if (!$unprotectedHeaders) { + $unprotectedHeaders = null; + } elseif (count($protectedHeaders) !== count($unprotectedHeaders)) { + throw new \InvalidArgumentException('There has to be equal amount of protected and unprotected headers'); + } else { + $unprotectedHeaders = array_values($unprotectedHeaders); + } + $this->unprotectedHeaders = $unprotectedHeaders; + } + + /** + * @inheritDoc + */ + public function getProtectedHeaders(): array + { + return $this->protectedHeaders; + } + + /** + * @inheritDoc + */ + public function getUnprotectedHeaders(): ?array + { + return $this->unprotectedHeaders; + } + + /** + * @inheritDoc + */ + public function getHeader(): HeaderInterface + { + return $this->protectedHeaders[0]; + } + + /** + * @inheritDoc + */ + public function getPayload(): PayloadInterface + { + return $this->payload; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwtInterface.php b/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwtInterface.php index ada5be1e1aca4..06ed6a5d3814b 100644 --- a/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwtInterface.php +++ b/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwtInterface.php @@ -8,6 +8,7 @@ namespace Magento\Framework\Jwt\Unsecured; +use Magento\Framework\Jwt\HeaderInterface; use Magento\Framework\Jwt\JwtInterface; /** @@ -15,5 +16,19 @@ */ interface UnsecuredJwtInterface extends JwtInterface { + /** + * Protected (not really) headers. + * + * Same as "[getHeader()]" for compact serialization. + * + * @return HeaderInterface[] + */ + public function getProtectedHeaders(): array; + /** + * Unprotected header can be present when JSON serialization is employed. + * + * @return HeaderInterface[]|null + */ + public function getUnprotectedHeaders(): ?array; } From 7339e6905d2b9a806e99f01aa539c0f837b5b007 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Thu, 4 Feb 2021 14:10:15 -0600 Subject: [PATCH 034/137] MC-38539: Introduce JWT wrapper --- .../JwtFrameworkAdapter/Model/Data/Claim.php | 62 +------------ .../Framework/Jwt/Claim/AbstractClaim.php | 91 +++++++++++++++++++ .../Magento/Framework/Jwt/Claim/Audience.php | 2 +- .../Framework/Jwt/Claim/ExpirationTime.php | 2 +- .../Magento/Framework/Jwt/Claim/IssuedAt.php | 2 +- .../Magento/Framework/Jwt/Claim/Issuer.php | 2 +- .../Magento/Framework/Jwt/Claim/JwtId.php | 2 +- .../Magento/Framework/Jwt/Claim/NotBefore.php | 2 +- .../Framework/Jwt/Claim/PrivateClaim.php | 61 +------------ .../Framework/Jwt/Claim/PublicClaim.php | 69 ++------------ .../Magento/Framework/Jwt/Claim/Subject.php | 2 +- .../Magento/Framework/Jwt/ClaimInterface.php | 10 +- .../Magento/Framework/Jwt/Jws/AbstractJws.php | 88 ++++++++++++++++++ .../Magento/Framework/Jwt/Jws/Jws.php | 72 +-------------- lib/internal/Magento/Framework/Jwt/README.md | 3 + .../Jwt/Test/Unit/Claim/AbstractClaimTest.php | 40 ++++++++ .../Framework/Jwt/Unsecured/UnsecuredJwt.php | 72 +-------------- 17 files changed, 255 insertions(+), 327 deletions(-) create mode 100644 lib/internal/Magento/Framework/Jwt/Claim/AbstractClaim.php create mode 100644 lib/internal/Magento/Framework/Jwt/Jws/AbstractJws.php create mode 100644 lib/internal/Magento/Framework/Jwt/README.md create mode 100644 lib/internal/Magento/Framework/Jwt/Test/Unit/Claim/AbstractClaimTest.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php index ef3a7d7faff81..cd102609201dd 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/Data/Claim.php @@ -8,66 +8,12 @@ namespace Magento\JwtFrameworkAdapter\Model\Data; -use Magento\Framework\Jwt\ClaimInterface; +use Magento\Framework\Jwt\Claim\AbstractClaim; -class Claim implements ClaimInterface +class Claim extends AbstractClaim { - /** - * @var string - */ - private $name; - - /** - * @var mixed - */ - private $value; - - /** - * @var string|null - */ - private $class; - - /** - * @param string $name - * @param mixed $value - * @param string|null $class - */ - public function __construct(string $name, $value, ?string $class) - { - $this->name = $name; - $this->value = $value; - $this->class = $class; - } - - /** - * @inheritDoc - */ - public function getName(): string - { - return $this->name; - } - - /** - * @inheritDoc - */ - public function getValue() - { - return $this->value; - } - - /** - * @inheritDoc - */ - public function getClass(): ?string - { - return $this->class; - } - - /** - * @inheritDoc - */ - public function isHeaderDuplicated(): bool + public function __construct(string $name, $value, ?int $class) { - return false; + parent::__construct($name, $value, $class, false); } } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/AbstractClaim.php b/lib/internal/Magento/Framework/Jwt/Claim/AbstractClaim.php new file mode 100644 index 0000000000000..92e5934a9547b --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Claim/AbstractClaim.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Claim; + +use Magento\Framework\Jwt\ClaimInterface; + +/** + * Abstract user-defined claim. + */ +abstract class AbstractClaim implements ClaimInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var int|float|string|bool|array|null + */ + private $value; + + /** + * @var int|null + */ + private $class; + + /** + * @var bool + */ + private $duplicated; + + /** + * Parse NumericDate and return DateTime with UTC timezone. + * + * @param string $date + * @return \DateTimeInterface + */ + public static function parseNumericDate(string $date): \DateTimeInterface + { + $dt = \DateTime::createFromFormat('Y-m-d\TH:i:sT', $date); + $dt->setTimezone(new \DateTimeZone('UTC')); + + return \DateTimeImmutable::createFromMutable($dt); + } + + public function __construct(string $name, $value, ?int $class, bool $duplicated = false) + { + $this->name = $name; + $this->value = $value; + $this->class = $class; + $this->duplicated = $duplicated; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function getClass(): ?int + { + return $this->class; + } + + /** + * @inheritDoc + */ + public function isHeaderDuplicated(): bool + { + return $this->duplicated; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Claim/Audience.php b/lib/internal/Magento/Framework/Jwt/Claim/Audience.php index 3bfce6f29a9f2..2d4f5099d7296 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/Audience.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/Audience.php @@ -57,7 +57,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php b/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php index bc02019ec7462..fe3c8da24af60 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/ExpirationTime.php @@ -58,7 +58,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php b/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php index 9dafcabba7738..6e4c94e0cc350 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/IssuedAt.php @@ -58,7 +58,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php b/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php index c78b5294019e8..16befd23a9963 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/Issuer.php @@ -54,7 +54,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php b/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php index 8964a1f7f35f4..3fab77c915df3 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/JwtId.php @@ -54,7 +54,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php b/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php index 53dff7045229b..db4a21421efef 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/NotBefore.php @@ -58,7 +58,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php b/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php index 90e62689a8866..821ff944d5c00 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/PrivateClaim.php @@ -8,69 +8,18 @@ namespace Magento\Framework\Jwt\Claim; -use Magento\Framework\Jwt\ClaimInterface; - /** * Private non-registered claim. */ -class PrivateClaim implements ClaimInterface +class PrivateClaim extends AbstractClaim { - /** - * @var string - */ - private $name; - - /** - * @var mixed - */ - private $value; - - /** - * @var bool - */ - private $headerDuplicated; - /** * @param string $name - * @param mixed $value - * @param bool $headerDuplicated - */ - public function __construct(string $name, $value, bool $headerDuplicated = false) - { - $this->name = $name; - $this->value = $value; - $this->headerDuplicated = $headerDuplicated; - } - - /** - * @inheritDoc - */ - public function getName(): string - { - return $this->name; - } - - /** - * @inheritDoc - */ - public function getValue() - { - return $this->value; - } - - /** - * @inheritDoc - */ - public function getClass(): ?string - { - return self::CLASS_PRIVATE; - } - - /** - * @inheritDoc + * @param $value + * @param bool $duplicated */ - public function isHeaderDuplicated(): bool + public function __construct(string $name, $value, bool $duplicated = false) { - return $this->headerDuplicated; + parent::__construct($name, $value, self::CLASS_PRIVATE, $duplicated); } } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php b/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php index 35e0a4a21320a..993d03f027dda 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/PublicClaim.php @@ -8,67 +8,16 @@ namespace Magento\Framework\Jwt\Claim; -use Magento\Framework\Jwt\ClaimInterface; - -class PublicClaim implements ClaimInterface +/** + * Public collision-resistant claim. + */ +class PublicClaim extends AbstractClaim { - /** - * @var string - */ - private $name; - - /** - * @var mixed - */ - private $value; - - /** - * @var bool - */ - private $headerDuplicated; - - /** - * @param string $name - * @param mixed $value - * @param string|null $prefix - * @param bool $headerDuplicated - */ - public function __construct(string $name, $value, ?string $prefix, bool $headerDuplicated = false) - { - $this->name = $prefix ? $prefix .'-' .$name : $name; - $this->value = $value; - $this->headerDuplicated = $headerDuplicated; - } - - /** - * @inheritDoc - */ - public function getName(): string - { - return $this->name; - } - - /** - * @inheritDoc - */ - public function getValue() - { - return $this->value; - } - - /** - * @inheritDoc - */ - public function getClass(): ?string - { - return self::CLASS_PUBLIC; - } - - /** - * @inheritDoc - */ - public function isHeaderDuplicated(): bool + public function __construct(string $name, $value, ?string $prefix, bool $duplicated = false) { - return $this->headerDuplicated; + if ($prefix) { + $prefix .= '-'; + } + parent::__construct($prefix .$name, $value, self::CLASS_PUBLIC, $duplicated); } } diff --git a/lib/internal/Magento/Framework/Jwt/Claim/Subject.php b/lib/internal/Magento/Framework/Jwt/Claim/Subject.php index b6c479d6fbb23..2a1f12ab71dc3 100644 --- a/lib/internal/Magento/Framework/Jwt/Claim/Subject.php +++ b/lib/internal/Magento/Framework/Jwt/Claim/Subject.php @@ -54,7 +54,7 @@ public function getValue() /** * @inheritDoc */ - public function getClass(): ?string + public function getClass(): ?int { return self::CLASS_REGISTERED; } diff --git a/lib/internal/Magento/Framework/Jwt/ClaimInterface.php b/lib/internal/Magento/Framework/Jwt/ClaimInterface.php index 739c1b46dc244..2b0278247a0fb 100644 --- a/lib/internal/Magento/Framework/Jwt/ClaimInterface.php +++ b/lib/internal/Magento/Framework/Jwt/ClaimInterface.php @@ -13,11 +13,11 @@ */ interface ClaimInterface { - public const CLASS_REGISTERED = 'registered'; + public const CLASS_REGISTERED = 1; - public const CLASS_PUBLIC = 'public'; + public const CLASS_PUBLIC = 2; - public const CLASS_PRIVATE = 'private'; + public const CLASS_PRIVATE = 3; /** * Claim name. @@ -36,9 +36,9 @@ public function getValue(); /** * Claim class when possible to identify. * - * @return string|null + * @return int|null */ - public function getClass(): ?string; + public function getClass(): ?int; /** * Whether to duplicate this claim to JOSE header. diff --git a/lib/internal/Magento/Framework/Jwt/Jws/AbstractJws.php b/lib/internal/Magento/Framework/Jwt/Jws/AbstractJws.php new file mode 100644 index 0000000000000..6cd71c6cb8427 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Jws/AbstractJws.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Jws; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\JwtInterface; +use Magento\Framework\Jwt\PayloadInterface; + +/** + * Abstract JWS DTO. + */ +abstract class AbstractJws implements JwtInterface +{ + /** + * @var HeaderInterface[] + */ + private $protectedHeaders; + + /** + * @var HeaderInterface[]|null + */ + private $unprotectedHeaders; + + /** + * @var PayloadInterface + */ + private $payload; + + /** + * @param HeaderInterface[] $protectedHeaders + * @param PayloadInterface $payload + * @param HeaderInterface[]|null $unprotectedHeaders + */ + public function __construct(array $protectedHeaders, PayloadInterface $payload, ?array $unprotectedHeaders) + { + if (!$protectedHeaders) { + throw new \InvalidArgumentException('Need at least 1 header'); + } + $this->protectedHeaders = array_values($protectedHeaders); + $this->payload = $payload; + if (!$unprotectedHeaders) { + $unprotectedHeaders = null; + } elseif (count($protectedHeaders) !== count($unprotectedHeaders)) { + throw new \InvalidArgumentException('There has to be equal amount of protected and unprotected headers'); + } else { + $unprotectedHeaders = array_values($unprotectedHeaders); + } + $this->unprotectedHeaders = $unprotectedHeaders; + } + + /** + * @inheritDoc + */ + public function getProtectedHeaders(): array + { + return $this->protectedHeaders; + } + + /** + * @inheritDoc + */ + public function getUnprotectedHeaders(): ?array + { + return $this->unprotectedHeaders; + } + + /** + * @inheritDoc + */ + public function getHeader(): HeaderInterface + { + return $this->protectedHeaders[0]; + } + + /** + * @inheritDoc + */ + public function getPayload(): PayloadInterface + { + return $this->payload; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Jws/Jws.php b/lib/internal/Magento/Framework/Jwt/Jws/Jws.php index 9856fcfb014b5..a03bb3bad8a95 100644 --- a/lib/internal/Magento/Framework/Jwt/Jws/Jws.php +++ b/lib/internal/Magento/Framework/Jwt/Jws/Jws.php @@ -8,77 +8,7 @@ namespace Magento\Framework\Jwt\Jws; -use Magento\Framework\Jwt\HeaderInterface; -use Magento\Framework\Jwt\PayloadInterface; - -class Jws implements JwsInterface +class Jws extends AbstractJws implements JwsInterface { - /** - * @var HeaderInterface[] - */ - private $protectedHeaders; - - /** - * @var HeaderInterface[]|null - */ - private $unprotectedHeaders; - - /** - * @var PayloadInterface - */ - private $payload; - - /** - * @param HeaderInterface[] $protectedHeaders - * @param PayloadInterface $payload - * @param HeaderInterface[]|null $unprotectedHeaders - */ - public function __construct(array $protectedHeaders, PayloadInterface $payload, ?array $unprotectedHeaders) - { - if (!$protectedHeaders) { - throw new \InvalidArgumentException('Need at least 1 header'); - } - $this->protectedHeaders = array_values($protectedHeaders); - $this->payload = $payload; - if (!$unprotectedHeaders) { - $unprotectedHeaders = null; - } elseif (count($protectedHeaders) !== count($unprotectedHeaders)) { - throw new \InvalidArgumentException('There has to be equal amount of protected and unprotected headers'); - } else { - $unprotectedHeaders = array_values($unprotectedHeaders); - } - $this->unprotectedHeaders = $unprotectedHeaders; - } - - /** - * @inheritDoc - */ - public function getProtectedHeaders(): array - { - return $this->protectedHeaders; - } - - /** - * @inheritDoc - */ - public function getUnprotectedHeaders(): ?array - { - return $this->unprotectedHeaders; - } - - /** - * @inheritDoc - */ - public function getHeader(): HeaderInterface - { - return $this->protectedHeaders[0]; - } - /** - * @inheritDoc - */ - public function getPayload(): PayloadInterface - { - return $this->payload; - } } diff --git a/lib/internal/Magento/Framework/Jwt/README.md b/lib/internal/Magento/Framework/Jwt/README.md new file mode 100644 index 0000000000000..09eba2907f84a --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/README.md @@ -0,0 +1,3 @@ +# JWT + +**JWT** module provides abstraction to work with JWTs along with useful utilities. diff --git a/lib/internal/Magento/Framework/Jwt/Test/Unit/Claim/AbstractClaimTest.php b/lib/internal/Magento/Framework/Jwt/Test/Unit/Claim/AbstractClaimTest.php new file mode 100644 index 0000000000000..00d93afc0ef28 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Test/Unit/Claim/AbstractClaimTest.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Test\Unit\Claim; + +use Magento\Framework\Jwt\Claim\AbstractClaim; +use PHPUnit\Framework\TestCase; + +class AbstractClaimTest extends TestCase +{ + /** + * Test parsing NumericDate JS format. + * + * @param string $numericDate + * @param string $expectedTime + * @param string $expectedZone + * @return void + * + * @dataProvider getDates + */ + public function testParseNumericDate(string $numericDate, string $expectedTime): void + { + $dt = AbstractClaim::parseNumericDate($numericDate); + $this->assertEquals($expectedTime, $dt->format('Y-m-d H:i:s')); + $this->assertEquals('UTC', $dt->getTimezone()->getName()); + } + + public function getDates(): array + { + return [ + ['1970-01-01T00:00:00Z', '1970-01-01 00:00:00'], + ['1996-12-19T16:39:57-08:00', '1996-12-20 00:39:57'] + ]; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php b/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php index 47523a839cba7..f5834063c5bf0 100644 --- a/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php +++ b/lib/internal/Magento/Framework/Jwt/Unsecured/UnsecuredJwt.php @@ -8,77 +8,9 @@ namespace Magento\Framework\Jwt\Unsecured; -use Magento\Framework\Jwt\HeaderInterface; -use Magento\Framework\Jwt\PayloadInterface; +use Magento\Framework\Jwt\Jws\AbstractJws; -class UnsecuredJwt implements UnsecuredJwtInterface +class UnsecuredJwt extends AbstractJws implements UnsecuredJwtInterface { - /** - * @var HeaderInterface[] - */ - private $protectedHeaders; - /** - * @var HeaderInterface[]|null - */ - private $unprotectedHeaders; - - /** - * @var PayloadInterface - */ - private $payload; - - /** - * @param HeaderInterface[] $protectedHeaders - * @param PayloadInterface $payload - * @param HeaderInterface[]|null $unprotectedHeaders - */ - public function __construct(array $protectedHeaders, PayloadInterface $payload, ?array $unprotectedHeaders) - { - if (!$protectedHeaders) { - throw new \InvalidArgumentException('Need at least 1 header'); - } - $this->protectedHeaders = array_values($protectedHeaders); - $this->payload = $payload; - if (!$unprotectedHeaders) { - $unprotectedHeaders = null; - } elseif (count($protectedHeaders) !== count($unprotectedHeaders)) { - throw new \InvalidArgumentException('There has to be equal amount of protected and unprotected headers'); - } else { - $unprotectedHeaders = array_values($unprotectedHeaders); - } - $this->unprotectedHeaders = $unprotectedHeaders; - } - - /** - * @inheritDoc - */ - public function getProtectedHeaders(): array - { - return $this->protectedHeaders; - } - - /** - * @inheritDoc - */ - public function getUnprotectedHeaders(): ?array - { - return $this->unprotectedHeaders; - } - - /** - * @inheritDoc - */ - public function getHeader(): HeaderInterface - { - return $this->protectedHeaders[0]; - } - - /** - * @inheritDoc - */ - public function getPayload(): PayloadInterface - { - return $this->payload; - } } From 5e84a824935820287595346c4c6c8455a47ec74f Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Thu, 4 Feb 2021 14:51:49 -0600 Subject: [PATCH 035/137] MC-38539: Introduce JWT wrapper --- .../JwtFrameworkAdapter/Model/JweManager.php | 42 ++++ .../JwtFrameworkAdapter/Model/JwsManager.php | 38 +++ .../JwtFrameworkAdapter/Model/JwtManager.php | 13 + .../Magento/Framework/Jwt/JwtManagerTest.php | 231 ++++++++++++++++++ .../Framework/Jwt/JwtManagerInterface.php | 8 + 5 files changed, 332 insertions(+) diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php index 805785ba17989..552bc6616341e 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweManager.php @@ -17,11 +17,14 @@ use Magento\Framework\Jwt\Exception\MalformedTokenException; use Magento\Framework\Jwt\HeaderInterface; use Magento\Framework\Jwt\Jwe\JweEncryptionJwks; +use Magento\Framework\Jwt\Jwe\JweHeader; use Magento\Framework\Jwt\Jwe\JweInterface; use Jose\Component\Core\JWK as AdapterJwk; use Jose\Component\Core\JWKSet as AdapterJwkSet; use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\Jws\JwsHeader; use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; +use Magento\JwtFrameworkAdapter\Model\Data\Header; /** * Works with JWE @@ -170,6 +173,45 @@ function (Jwk $jwk) { ); } + /** + * Read JWS headers. + * + * @param string $token + * @return HeaderInterface[] + */ + public function readHeaders(string $token): array + { + try { + $jwe = $this->serializer->unserialize($token); + } catch (\Throwable $exception) { + throw new JwtException('Failed to read JWE headers'); + } + $headers = []; + $headersValues = []; + if ($jwe->getSharedHeader()) { + $headersValues[] = $jwe->getSharedHeader(); + } + if ($jwe->getSharedProtectedHeader()) { + $headersValues[] = $jwe->getSharedProtectedHeader(); + } + foreach ($jwe->getRecipients() as $recipient) { + if ($recipient->getHeader()) { + $headersValues[] = $recipient->getHeader(); + } + } + foreach ($headersValues as $headerValues) { + $params = []; + foreach ($headerValues as $header => $value) { + $params[] = new Header($header, $value, null); + } + if ($params) { + $headers[] = new JweHeader($params); + } + } + + return $headers; + } + private function validateJweSettings(JweInterface $jwe, EncryptionSettingsInterface $encryptionSettings): void { if (!$encryptionSettings instanceof JweEncryptionJwks) { diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php index dbe8852a19d2d..b5e03be38dcc0 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsManager.php @@ -17,10 +17,12 @@ use Magento\Framework\Jwt\Exception\MalformedTokenException; use Magento\Framework\Jwt\HeaderInterface; use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\Jws\JwsHeader; use Magento\Framework\Jwt\Jws\JwsInterface; use Magento\Framework\Jwt\Jws\JwsSignatureJwks; use Jose\Component\Core\JWK as AdapterJwk; use Jose\Component\Core\JWKSet as AdapterJwkSet; +use Magento\JwtFrameworkAdapter\Model\Data\Header; /** * Works with JWS. @@ -167,6 +169,42 @@ function (Jwk $jwk) { ); } + /** + * Read JWS headers. + * + * @param string $token + * @return HeaderInterface[] + */ + public function readHeaders(string $token): array + { + try { + $jws = $this->jwsSerializer->unserialize($token); + } catch (\Throwable $exception) { + throw new JwtException('Failed to read JWS headers'); + } + $headers = []; + $headersValues = []; + foreach ($jws->getSignatures() as $signature) { + if ($signature->getProtectedHeader()) { + $headersValues[] = $signature->getProtectedHeader(); + } + if ($signature->getHeader()) { + $headersValues[] = $signature->getHeader(); + } + } + foreach ($headersValues as $headerValues) { + $params = []; + foreach ($headerValues as $header => $value) { + $params[] = new Header($header, $value, null); + } + if ($params) { + $headers[] = new JwsHeader($params); + } + } + + return $headers; + } + /** * Extract JOSE header data. * diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php index fb23dd36bd8e7..56208f349a705 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php @@ -12,6 +12,7 @@ use Magento\Framework\Jwt\Exception\EncryptionException; use Magento\Framework\Jwt\Exception\JwtException; use Magento\Framework\Jwt\Exception\MalformedTokenException; +use Magento\Framework\Jwt\HeaderInterface; use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface; use Magento\Framework\Jwt\Jwe\JweInterface; use Magento\Framework\Jwt\Jwk; @@ -159,6 +160,18 @@ public function read(string $token, array $acceptableEncryption): JwtInterface return $read; } + /** + * @inheritDoc + */ + public function readHeaders(string $token): array + { + try { + return $this->jwsManager->readHeaders($token); + } catch (JwtException $exception) { + return $this->jweManager->readHeaders($token); + } + } + private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): int { if ($encryptionSettings instanceof JwsSignatureSettingsInterface) { diff --git a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php index 5c3c389bbb5c7..29c3fd2b6559a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php @@ -715,6 +715,237 @@ public function getTokenVariants(): array ]; } + /** + * Test reading headers. + * + * @param JwtInterface $tokenData + * @param EncryptionSettingsInterface $settings + * @return void + * + * @dataProvider getJwtsForHeaders + */ + public function testReadHeaders(JwtInterface $tokenData, EncryptionSettingsInterface $settings): void + { + $token = $this->manager->create($tokenData, $settings); + $headers = $this->manager->readHeaders($token); + /** @var HeaderInterface[] $expectedHeaders */ + $expectedHeaders = []; + if ($tokenData instanceof JwsInterface) { + $expectedHeaders = $tokenData->getProtectedHeaders(); + if ($tokenData->getUnprotectedHeaders()) { + $expectedHeaders = array_merge($expectedHeaders, $tokenData->getUnprotectedHeaders()); + } + } elseif ($tokenData instanceof JweInterface) { + $expectedHeaders[] = $tokenData->getProtectedHeader(); + if ($tokenData->getSharedUnprotectedHeader()) { + $expectedHeaders[] = $tokenData->getSharedUnprotectedHeader(); + } + if ($tokenData->getPerRecipientUnprotectedHeaders()) { + $expectedHeaders = array_merge($expectedHeaders, $tokenData->getPerRecipientUnprotectedHeaders()); + } + } elseif ($tokenData instanceof UnsecuredJwtInterface) { + $expectedHeaders = $tokenData->getProtectedHeaders(); + if ($tokenData->getUnprotectedHeaders()) { + $expectedHeaders = array_merge($expectedHeaders, $tokenData->getUnprotectedHeaders()); + } + } + + foreach ($headers as $header) { + $this->verifyAgainstHeaders($expectedHeaders, $header); + } + } + + public function getJwtsForHeaders(): array + { + + /** @var JwkFactory $jwkFactory */ + $jwkFactory = Bootstrap::getObjectManager()->get(JwkFactory::class); + + $flatJws = new Jws( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('custom-header', 'value'), + new PrivateHeaderParameter('another-custom-header', 'value2') + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2'), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ), + null + ); + $flatJsonJws = new Jws( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('custom-header', 'value'), + new Critical(['magento']) + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2'), + new ExpirationTime(new \DateTimeImmutable()) + ] + ), + [ + new JwsHeader( + [ + new PublicHeaderParameter('public-header', 'magento', 'public-value') + ] + ) + ] + ); + $jsonJws = new Jws( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + new JwsHeader( + [ + new PrivateHeaderParameter('test3', true), + new PublicHeaderParameter('test4', 'magento', 'value-another') + ] + ) + ], + new ClaimsPayload([ + new Issuer('magento.com'), + new JwtId(), + new Subject('stuff') + ]), + [ + new JwsHeader([new PrivateHeaderParameter('public', 'header1')]), + new JwsHeader([new PrivateHeaderParameter('public2', 'header')]) + ] + ); + $flatJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + null, + null, + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); + $jsonFlatJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + null, + [ + new JweHeader( + [ + new PrivateHeaderParameter('mage', 'test') + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); + $jsonJwe = new Jwe( + new JweHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ), + new JweHeader( + [ + new PrivateHeaderParameter('mage', 'test') + ] + ), + [ + new JweHeader([new PrivateHeaderParameter('tst', 2)]), + new JweHeader([new PrivateHeaderParameter('test2', 3)]) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ) + ); + $flatUnsecured = new UnsecuredJwt( + [ + new JwsHeader( + [ + new PrivateHeaderParameter('test', true), + new PublicHeaderParameter('test2', 'magento', 'value') + ] + ) + ], + new ClaimsPayload( + [ + new PrivateClaim('custom-claim', 'value'), + new PrivateClaim('custom-claim2', 'value2', true), + new PrivateClaim('custom-claim3', 'value3'), + new IssuedAt(new \DateTimeImmutable()), + new Issuer('magento.com') + ] + ), + null + ); + + $sharedSecret = random_bytes(2048); + $jwsJwk = $jwkFactory->createHs256($sharedSecret); + $jweJwk = $jwkFactory->createA128KW($sharedSecret); + $jwsSettings = new JwsSignatureJwks($jwsJwk); + $jsonJwsSettings = new JwsSignatureJwks(new JwkSet([$jwsJwk, $jwsJwk])); + $jweJwkSettings = new JweEncryptionJwks( + $jweJwk, + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ); + $jsonJweSettings = new JweEncryptionJwks( + new JwkSet([$jweJwk, $jweJwk]), + JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM + ); + + return [ + 'jws' => [$flatJws, $jwsSettings], + 'flat-jws' => [$flatJsonJws, $jwsSettings], + 'json-jws' => [$jsonJws, $jsonJwsSettings], + 'jwe' => [$flatJwe, $jweJwkSettings], + 'flat-jwe' => [$jsonFlatJwe, $jweJwkSettings], + 'json-jwe' => [$jsonJwe, $jsonJweSettings], + 'none-jws' => [$flatUnsecured, new NoEncryption()] + ]; + } + private function validateHeader(HeaderInterface $expected, HeaderInterface $actual): void { if (count($expected->getParameters()) > count($actual->getParameters())) { diff --git a/lib/internal/Magento/Framework/Jwt/JwtManagerInterface.php b/lib/internal/Magento/Framework/Jwt/JwtManagerInterface.php index 1079e9a27cd8e..8387ddfba7e33 100644 --- a/lib/internal/Magento/Framework/Jwt/JwtManagerInterface.php +++ b/lib/internal/Magento/Framework/Jwt/JwtManagerInterface.php @@ -34,4 +34,12 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio * @throws JwtException */ public function read(string $token, array $acceptableEncryption): JwtInterface; + + /** + * Read unprotected headers. + * + * @param string $token + * @return HeaderInterface[] + */ + public function readHeaders(string $token): array; } From b629ddb5d58709b150157f1ba98decbf62035898 Mon Sep 17 00:00:00 2001 From: Sergiy Vasiutynskyi <s.vasiutynskyi@atwix.com> Date: Tue, 9 Feb 2021 16:57:32 +0200 Subject: [PATCH 036/137] changed values for CliCacheFlushActionGroup --- .../Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml | 4 +++- .../Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 0f4c8bebca17d..5b8691e55b3ae 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -147,7 +147,9 @@ <!-- Disable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 0" stepKey="disableTooltips"/> - <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDisabling"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisabling"> + <argument name="tags" value="config full_page"/> + </actionGroup> <!-- Verify swatch tooltips are not visible --> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index 6e877baf8ffc2..952918926a63c 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -116,7 +116,7 @@ <!-- 2. Refresh magento cache --> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateEnabled"> - <argument name="tags" value="full_page"/> + <argument name="tags" value="translate full_page"/> </actionGroup> <!-- 3. Go to storefront and click on cart button on the top --> @@ -478,7 +478,9 @@ <!-- 7. Set *Enabled for Storefront* option to *No* and save configuration --> <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> <!-- 8. Clear magento cache --> - <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterTranslateDisabled"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateDisabled"> + <argument name="tags" value="translate full_page"/> + </actionGroup> <magentoCLI command="setup:static-content:deploy -f" stepKey="deployStaticContent"/> From 35468fcf492f78ce46a31b92be4fb5cd2f0ae9d5 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Tue, 16 Feb 2021 14:31:40 +0200 Subject: [PATCH 037/137] magento/magento2#32116 --- .../Plugin/RemoveSubscriberFromQueue.php | 55 +++++++++++++++ app/code/Magento/Newsletter/etc/di.xml | 4 ++ .../Newsletter/Model/SubscriberTest.php | 70 +++++++++++++++---- 3 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php diff --git a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php new file mode 100644 index 0000000000000..1e928a1c4b37e --- /dev/null +++ b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Model\Plugin; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Newsletter\Model\Subscriber; + +/** + * Plugin responsible for removing subscriber from queue after unsubscribe + */ +class RemoveSubscriberFromQueue +{ + private const STATUS = 'subscriber_status'; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource->getConnection(); + } + + /** + * Removes subscriber from queue + * + * @param Subscriber $subject + * @param Subscriber $subscriber + * @return Subscriber + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUnsubscribe(Subscriber $subject, Subscriber $subscriber): Subscriber + { + if ($subscriber->dataHasChangedFor(self::STATUS) + && $subscriber->getSubscriberStatus() === Subscriber::STATUS_UNSUBSCRIBED + ) { + $this->connection->delete( + $this->connection->getTableName('newsletter_queue_link'), + ['subscriber_id = ?' => $subscriber->getId(), 'letter_sent_at IS NULL'] + ); + } + + return $subscriber; + } +} diff --git a/app/code/Magento/Newsletter/etc/di.xml b/app/code/Magento/Newsletter/etc/di.xml index 3c35936a2e8aa..e630024853bc3 100644 --- a/app/code/Magento/Newsletter/etc/di.xml +++ b/app/code/Magento/Newsletter/etc/di.xml @@ -32,4 +32,8 @@ </type> <preference for="Magento\Newsletter\Model\SubscriptionManagerInterface" type="Magento\Newsletter\Model\SubscriptionManager"/> + <type name="Magento\Newsletter\Model\Subscriber"> + <plugin name="remove_subscriber_from_queue_after_unsubscribe" + type="Magento\Newsletter\Model\Plugin\RemoveSubscriberFromQueue"/> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php index dbf8bce795548..6d40edc2a174f 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php @@ -8,10 +8,10 @@ namespace Magento\Newsletter\Model; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Framework\Mail\EmailMessage; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Mail\EmailMessage; +use Magento\Newsletter\Model\ResourceModel\Subscriber\CollectionFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use PHPUnit\Framework\TestCase; @@ -26,27 +26,42 @@ class SubscriberTest extends TestCase private const CONFIRMATION_SUBSCRIBE = 'You have been successfully subscribed to our newsletter.'; private const CONFIRMATION_UNSUBSCRIBE = 'You have been unsubscribed from the newsletter.'; - /** @var ObjectManagerInterface */ - private $objectManager; - - /** @var SubscriberFactory */ + /** + * @var SubscriberFactory + */ private $subscriberFactory; - /** @var TransportBuilderMock */ + /** + * @var TransportBuilderMock + */ private $transportBuilder; - /** @var CustomerRepositoryInterface */ + /** + * @var CustomerRepositoryInterface + */ private $customerRepository; + /** + * @var QueueFactory + */ + private $queueFactory; + + /** + * @var CollectionFactory + */ + private $subscriberCollectionFactory; + /** * @inheritdoc */ protected function setUp(): void { - $this->objectManager = Bootstrap::getObjectManager(); - $this->subscriberFactory = $this->objectManager->get(SubscriberFactory::class); - $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); - $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $objectManager = Bootstrap::getObjectManager(); + $this->subscriberFactory = $objectManager->get(SubscriberFactory::class); + $this->transportBuilder = $objectManager->get(TransportBuilderMock::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); + $this->queueFactory = $objectManager->get(QueueFactory::class); + $this->subscriberCollectionFactory = $objectManager->get(CollectionFactory::class); } /** @@ -157,6 +172,37 @@ public function testConfirm(): void ); } + /** + * Unsubscribe and check queue + * + * @magentoDataFixture Magento/Newsletter/_files/queue.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUnsubscribeCustomer(): void + { + $firstSubscriber = $this->subscriberFactory->create() + ->load('customer@example.com', 'subscriber_email'); + $secondSubscriber = $this->subscriberFactory->create() + ->load('customer_two@example.com', 'subscriber_email'); + + $queue = $this->queueFactory->create() + ->load('CustomerSupport', 'newsletter_sender_name'); + $queue->addSubscribersToQueue([$firstSubscriber->getId(), $secondSubscriber->getId()]); + + $secondSubscriber->unsubscribe(); + + $collection = $this->subscriberCollectionFactory->create() + ->useQueue($queue); + + $this->assertCount(1, $collection); + $this->assertEquals( + 'customer@example.com', + $collection->getFirstItem()->getData('subscriber_email') + ); + } + /** * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php * @magentoDataFixture Magento/Newsletter/_files/newsletter_unconfirmed_customer.php From 23bfcdd84291ed84ebb6c06d820a92cd57116039 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 17 Feb 2021 14:53:59 +0200 Subject: [PATCH 038/137] added method for removing a subscriber from the queue into the resource model; refactor --- .../Plugin/RemoveSubscriberFromQueue.php | 22 +++++++------------ .../Newsletter/Model/ResourceModel/Queue.php | 16 +++++++++++++- .../Magento/Newsletter/Model/Subscriber.php | 7 +++++- app/code/Magento/Newsletter/etc/di.xml | 5 +---- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php index 1e928a1c4b37e..62437a35ce7a8 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php +++ b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php @@ -7,8 +7,7 @@ namespace Magento\Newsletter\Model\Plugin; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Newsletter\Model\ResourceModel\Queue as QueueResource; use Magento\Newsletter\Model\Subscriber; /** @@ -19,16 +18,16 @@ class RemoveSubscriberFromQueue private const STATUS = 'subscriber_status'; /** - * @var AdapterInterface + * @var QueueResource */ - private $connection; + private $queueResource; /** - * @param ResourceConnection $resource + * @param QueueResource $queueResource */ - public function __construct(ResourceConnection $resource) + public function __construct(QueueResource $queueResource) { - $this->connection = $resource->getConnection(); + $this->queueResource = $queueResource; } /** @@ -41,13 +40,8 @@ public function __construct(ResourceConnection $resource) */ public function afterUnsubscribe(Subscriber $subject, Subscriber $subscriber): Subscriber { - if ($subscriber->dataHasChangedFor(self::STATUS) - && $subscriber->getSubscriberStatus() === Subscriber::STATUS_UNSUBSCRIBED - ) { - $this->connection->delete( - $this->connection->getTableName('newsletter_queue_link'), - ['subscriber_id = ?' => $subscriber->getId(), 'letter_sent_at IS NULL'] - ); + if ($subscriber->isStatusChanged() && $subscriber->getSubscriberStatus() === Subscriber::STATUS_UNSUBSCRIBED) { + $this->queueResource->removeSubscriberFromQueue((int) $subscriber->getId()); } return $subscriber; diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php index 06ff66e290646..f327729c12018 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php @@ -86,7 +86,7 @@ public function addSubscribersToQueue(ModelQueue $queue, array $subscriberIds) $usedIds = array_flip($connection->fetchCol($select)); $subscriberIds = array_flip($subscriberIds); $newIds = array_diff_key($subscriberIds, $usedIds); - + $connection->beginTransaction(); try { foreach (array_keys($newIds) as $subscriberId) { @@ -125,6 +125,20 @@ public function removeSubscribersFromQueue(ModelQueue $queue) } } + /** + * Removes subscriber from queue + * + * @param int $subscriberId + * @return void + */ + public function removeSubscriberFromQueue(int $subscriberId): void + { + $this->getConnection()->delete( + $this->getTable('newsletter_queue_link'), + ['subscriber_id = ?' => $subscriberId, 'letter_sent_at IS NULL'] + ); + } + /** * Links queue to store * diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index c2d80f9000792..29045bf5a8e38 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -308,6 +308,10 @@ public function getStatus() */ public function setStatus($value) { + if ($this->getSubscriberStatus() !== $value) { + $this->setStatusChanged(true); + } + return $this->setSubscriberStatus($value); } @@ -449,7 +453,8 @@ public function unsubscribe() } if ($this->getSubscriberStatus() != self::STATUS_UNSUBSCRIBED) { - $this->setSubscriberStatus(self::STATUS_UNSUBSCRIBED)->save(); + $this->setStatus(self::STATUS_UNSUBSCRIBED); + $this->save(); $this->sendUnsubscriptionEmail(); } return $this; diff --git a/app/code/Magento/Newsletter/etc/di.xml b/app/code/Magento/Newsletter/etc/di.xml index e630024853bc3..cb97a6af7ddeb 100644 --- a/app/code/Magento/Newsletter/etc/di.xml +++ b/app/code/Magento/Newsletter/etc/di.xml @@ -26,14 +26,11 @@ type="Magento\Newsletter\Model\Plugin\CustomerPlugin"/> </type> <type name="Magento\Newsletter\Model\Subscriber"> + <plugin name="remove_subscriber_from_queue_after_unsubscribe" type="Magento\Newsletter\Model\Plugin\RemoveSubscriberFromQueue"/> <arguments> <argument name="customerSession" xsi:type="object">Magento\Customer\Model\Session\Proxy</argument> </arguments> </type> <preference for="Magento\Newsletter\Model\SubscriptionManagerInterface" type="Magento\Newsletter\Model\SubscriptionManager"/> - <type name="Magento\Newsletter\Model\Subscriber"> - <plugin name="remove_subscriber_from_queue_after_unsubscribe" - type="Magento\Newsletter\Model\Plugin\RemoveSubscriberFromQueue"/> - </type> </config> From 90baa798d060462b73d9fec521958f997990919e Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 17 Feb 2021 20:19:14 -0600 Subject: [PATCH 039/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Magento/Framework/App/Router/Base.php | 2 +- .../App/Test/Unit/Router/BaseTest.php | 322 +++++++++--------- 2 files changed, 155 insertions(+), 169 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Router/Base.php b/lib/internal/Magento/Framework/App/Router/Base.php index a0a3f6f8fd4aa..3c2945d5dbe08 100644 --- a/lib/internal/Magento/Framework/App/Router/Base.php +++ b/lib/internal/Magento/Framework/App/Router/Base.php @@ -179,7 +179,7 @@ protected function parseRequest(\Magento\Framework\App\RequestInterface $request $path = trim($request->getPathInfo(), '/'); - $params = explode('/', $path ? $path : $this->pathConfig->getDefaultPath()); + $params = explode('/', strlen($path) ? $path : $this->pathConfig->getDefaultPath()); foreach ($this->_requiredParams as $paramName) { $output[$paramName] = array_shift($params); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php index 0c7454768b5f5..a03a3675b8738 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php @@ -1,49 +1,40 @@ -<?php declare(strict_types=1); +<?php /** - * Tests Magento\Framework\App\Router\Base - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\App\Test\Unit\Router; -use Magento\Framework\App\Action\Action; use Magento\Framework\App\ActionFactory; +use Magento\Framework\App\ActionInterface; use Magento\Framework\App\DefaultPathInterface; use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResponseFactory; use Magento\Framework\App\Route\ConfigInterface; use Magento\Framework\App\Router\ActionList; use Magento\Framework\App\Router\Base; -use Magento\Framework\App\State; +use Magento\Framework\App\Router\PathConfigInterface; use Magento\Framework\Code\NameBuilder; -use Magento\Framework\TestFramework\Unit\BaseTestCase; +use Magento\Framework\UrlInterface; use PHPUnit\Framework\MockObject\MockObject; /** * Base router unit test. */ -class BaseTest extends BaseTestCase +class BaseTest extends \PHPUnit\Framework\TestCase { /** * @var Base */ private $model; - /** - * @var MockObject|Http - */ - private $requestMock; - /** * @var MockObject|ConfigInterface */ private $routeConfigMock; - /** - * @var MockObject|State - */ - private $appStateMock; - /** * @var MockObject|ActionList */ @@ -64,175 +55,185 @@ class BaseTest extends BaseTestCase */ private $defaultPathMock; - protected function setUp(): void - { - parent::setUp(); - // Create mocks - $this->requestMock = $this->basicMock(Http::class); - $this->routeConfigMock = $this->basicMock(ConfigInterface::class); - $this->appStateMock = $this->getMockBuilder(State::class) - ->addMethods(['isInstalled']) - ->disableOriginalConstructor() - ->getMock(); - $this->actionListMock = $this->basicMock(ActionList::class); - $this->actionFactoryMock = $this->basicMock(ActionFactory::class); - $this->nameBuilderMock = $this->basicMock(NameBuilder::class); - $this->defaultPathMock = $this->basicMock(DefaultPathInterface::class); - - // Prepare SUT - $mocks = [ - 'actionList' => $this->actionListMock, - 'actionFactory' => $this->actionFactoryMock, - 'routeConfig' => $this->routeConfigMock, - 'appState' => $this->appStateMock, - 'nameBuilder' => $this->nameBuilderMock, - 'defaultPath' => $this->defaultPathMock, - ]; - $this->model = $this->objectManager->getObject(Base::class, $mocks); - } - - public function testMatch() - { - // Test Data - $actionInstance = 'action instance'; - $moduleFrontName = 'module front name'; - $actionPath = 'action path'; - $actionName = 'action name'; - $actionClassName = Action::class; - $moduleName = 'module name'; - $moduleList = [$moduleName]; - $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; - - // Stubs - $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); - $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); - $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); - $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); - $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); - $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(true); - $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); - $this->actionFactoryMock->expects($this->any())->method('create')->willReturn($actionInstance); + /** + * @var MockObject|ResponseFactory + */ + private $responseFactoryMock; - // Expectations and Test - $this->requestExpects('setModuleName', $moduleFrontName) - ->requestExpects('setControllerName', $actionPath) - ->requestExpects('setActionName', $actionName) - ->requestExpects('setControllerModule', $moduleName); + /** + * @var MockObject|UrlInterface + */ + private $urlMock; - $this->assertSame($actionInstance, $this->model->match($this->requestMock)); - } + /** + * @var MockObject|PathConfigInterface + */ + private $pathConfigMock; - public function testMatchUseParams() + protected function setUp(): void { - // Test Data - $actionInstance = 'action instance'; - $moduleFrontName = 'module front name'; - $actionPath = 'action path'; - $actionName = 'action name'; - $actionClassName = Action::class; - $moduleName = 'module name'; - $moduleList = [$moduleName]; - $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; - - // Stubs - $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); - $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); - $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(false); - $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); - $this->actionFactoryMock->expects($this->any())->method('create')->willReturn($actionInstance); - - // Expectations and Test - $this->requestExpects('setModuleName', $moduleFrontName) - ->requestExpects('setControllerName', $actionPath) - ->requestExpects('setActionName', $actionName) - ->requestExpects('setControllerModule', $moduleName); - - $this->assertSame($actionInstance, $this->model->match($this->requestMock)); + $this->routeConfigMock = $this->createMock(ConfigInterface::class); + $this->actionListMock = $this->createMock(ActionList::class); + $this->actionFactoryMock = $this->createMock(ActionFactory::class); + $this->nameBuilderMock = $this->createMock(NameBuilder::class); + $this->defaultPathMock = $this->createMock(DefaultPathInterface::class); + $this->responseFactoryMock = $this->createMock(ResponseFactory::class); + $this->urlMock = $this->createMock(UrlInterface::class); + $this->pathConfigMock = $this->createMock(PathConfigInterface::class); + + $this->model = new Base( + $this->actionListMock, + $this->actionFactoryMock, + $this->defaultPathMock, + $this->responseFactoryMock, + $this->routeConfigMock, + $this->urlMock, + $this->nameBuilderMock, + $this->pathConfigMock + ); } - public function testMatchUseDefaultPath() - { - // Test Data - $actionInstance = 'action instance'; - $moduleFrontName = 'module front name'; - $actionPath = 'action path'; - $actionName = 'action name'; - $actionClassName = Action::class; - $moduleName = 'module name'; - $moduleList = [$moduleName]; - $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; + /** + * @dataProvider matchDataProvider + * @param MockObject|Http $requestMock + * @param string $defaultPath + * @param string $moduleFrontName + * @param string $actionPath + * @param string $actionName + * @param string $moduleName + */ + public function testMatch( + MockObject $requestMock, + string $defaultPath, + string $moduleFrontName, + string $actionPath, + string $actionName, + string $moduleName + ) { + $actionInstance = 'Magento_TestFramework_ActionInstance'; - // Stubs $defaultReturnMap = [ ['module', $moduleFrontName], ['controller', $actionPath], ['action', $actionName], ]; - $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); - $this->defaultPathMock->expects($this->any())->method('getPart')->willReturnMap($defaultReturnMap); - $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); - $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(false); - $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); - $this->actionFactoryMock->expects($this->any())->method('create')->willReturn($actionInstance); + $this->defaultPathMock->method('getPart') + ->willReturnMap($defaultReturnMap); + $this->pathConfigMock->method('getDefaultPath') + ->willReturn($defaultPath); + $this->routeConfigMock->expects($this->once()) + ->method('getModulesByFrontName') + ->with($moduleFrontName) + ->willReturn([$moduleName]); + + $actionMock = $this->getMockBuilder(ActionInterface::class) + ->setMockClassName($actionInstance) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->actionListMock->expects($this->once()) + ->method('get') + ->with($moduleName) + ->willReturn($actionInstance); + $this->actionFactoryMock->expects($this->once()) + ->method('create') + ->with($actionInstance) + ->willReturn($actionMock); + + $requestMock->expects($this->once())->method('setModuleName')->with($moduleFrontName); + $requestMock->expects($this->once())->method('setControllerName')->with($actionPath); + $requestMock->expects($this->once())->method('setActionName')->with($actionName); + $requestMock->expects($this->once())->method('setControllerModule')->with($moduleName); + + $this->assertEquals($actionMock, $this->model->match($requestMock)); + } - // Expectations and Test - $this->requestExpects('setModuleName', $moduleFrontName) - ->requestExpects('setControllerName', $actionPath) - ->requestExpects('setActionName', $actionName) - ->requestExpects('setControllerModule', $moduleName); + public function matchDataProvider(): array + { + $moduleFrontName = 'module_front_name'; + $actionPath = 'action_path'; + $actionName = 'action_name'; + $moduleName = 'module_name'; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; - $this->assertSame($actionInstance, $this->model->match($this->requestMock)); + $requestMock = $this->createMock(Http::class); + $requestMock->expects($this->atLeastOnce())->method('getModuleName')->willReturn($moduleFrontName); + $requestMock->expects($this->atLeastOnce())->method('getControllerName')->willReturn($actionPath); + $requestMock->expects($this->atLeastOnce())->method('getActionName')->willReturn($actionName); + $requestMock->expects($this->atLeastOnce())->method('getPathInfo')->willReturn($paramList); + + $emptyRequestMock = $this->createMock(Http::class); + $emptyRequestMock->expects($this->atLeastOnce())->method('getModuleName')->willReturn(''); + $emptyRequestMock->expects($this->atLeastOnce())->method('getControllerName')->willReturn(''); + $emptyRequestMock->expects($this->atLeastOnce())->method('getActionName')->willReturn(''); + $emptyRequestMock->expects($this->atLeastOnce())->method('getPathInfo')->willReturn(''); + + $emptyRequestMock2 = clone $emptyRequestMock; + $emptyRequestMock2->expects($this->once())->method('getOriginalPathInfo')->willReturn(''); + + return [ + [$requestMock, '', $moduleFrontName, $actionPath, $actionName, $moduleName], + [$emptyRequestMock, $paramList, $moduleFrontName, $actionPath, $actionName, $moduleName], + [$emptyRequestMock2, '', $moduleFrontName, $actionPath, $actionName, $moduleName], + ]; } public function testMatchEmptyModuleList() { - // Test Data - $actionInstance = 'action instance'; $moduleFrontName = 'module front name'; $actionPath = 'action path'; $actionName = 'action name'; - $actionClassName = Action::class; - $emptyModuleList = []; $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; - // Stubs - $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); - $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); - $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($emptyModuleList); - $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); - $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); - $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(false); - $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); - $this->actionFactoryMock->expects($this->any())->method('create')->willReturn($actionInstance); - - // Test - $this->assertNull($this->model->match($this->requestMock)); + $requestMock = $this->createMock(Http::class); + $requestMock->expects($this->atLeastOnce()) + ->method('getModuleName') + ->willReturn($moduleFrontName); + $requestMock->expects($this->atLeastOnce()) + ->method('getPathInfo') + ->willReturn($paramList); + $this->routeConfigMock->expects($this->once()) + ->method('getModulesByFrontName') + ->with($moduleFrontName) + ->willReturn([]); + $this->actionListMock->expects($this->never())->method('get'); + $this->actionFactoryMock->expects($this->never())->method('create'); + + $this->assertNull($this->model->match($requestMock)); } public function testMatchEmptyActionInstance() { - // Test Data - $nullActionInstance = null; $moduleFrontName = 'module front name'; $actionPath = 'action path'; $actionName = 'action name'; - $actionClassName = Action::class; $moduleName = 'module name'; - $moduleList = [$moduleName]; $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; - // Stubs - $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); - $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); - $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); - $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); - $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); - $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(false); - $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); - $this->actionFactoryMock->expects($this->any())->method('create')->willReturn($nullActionInstance); - - // Expectations and Test - $this->assertNull($this->model->match($this->requestMock)); + $requestMock = $this->createMock(Http::class); + $requestMock->expects($this->atLeastOnce()) + ->method('getModuleName') + ->willReturn($moduleFrontName); + $requestMock->expects($this->atLeastOnce()) + ->method('getPathInfo') + ->willReturn($paramList); + $requestMock->expects($this->atLeastOnce()) + ->method('getControllerName') + ->willReturn($actionPath); + $requestMock->expects($this->once()) + ->method('getActionName') + ->willReturn($actionName); + $this->routeConfigMock->expects($this->once()) + ->method('getModulesByFrontName') + ->with($moduleFrontName) + ->willReturn([$moduleName]); + $this->actionListMock->expects($this->once()) + ->method('get') + ->with($moduleName) + ->willReturn(null); + $this->actionFactoryMock->expects($this->never()) + ->method('create'); + + $this->assertNull($this->model->match($requestMock)); } public function testGetActionClassName() @@ -247,19 +248,4 @@ public function testGetActionClassName() ->willReturn($className); $this->assertEquals($className, $this->model->getActionClassName($module, $actionPath)); } - - /** - * Generate a stub with an expected usage for the request mock object - * - * @param string $method - * @param string $with - * @return $this - */ - private function requestExpects($method, $with) - { - $this->requestMock->expects($this->once()) - ->method($method) - ->with($with); - return $this; - } } From d594fa7b706d83086e0a1cd04a5fbd2523603744 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Thu, 18 Feb 2021 11:09:52 +0200 Subject: [PATCH 040/137] removed disabling dbIsolation --- .../testsuite/Magento/Newsletter/Model/SubscriberTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php index 6d40edc2a174f..caf4d192a72e0 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php @@ -176,7 +176,6 @@ public function testConfirm(): void * Unsubscribe and check queue * * @magentoDataFixture Magento/Newsletter/_files/queue.php - * @magentoDbIsolation disabled * * @return void */ From 861594147d30ee8763013fdffb68791067f2a139 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Thu, 18 Feb 2021 12:37:30 +0200 Subject: [PATCH 041/137] magento/magento2#30756 added non implemented method to obsolete --- app/code/Magento/Authorization/Model/Role.php | 58 +++++++------------ .../Test/Legacy/_files/obsolete_methods.php | 1 + 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index 96cf956afd1bc..74b84f1a7d311 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -5,28 +5,32 @@ */ namespace Magento\Authorization\Model; +use Magento\Authorization\Model\ResourceModel\Role\Collection; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Model\AbstractModel; + /** * Admin Role Model * * @api * @method int getParentId() - * @method \Magento\Authorization\Model\Role setParentId(int $value) + * @method Role setParentId(int $value) * @method int getTreeLevel() - * @method \Magento\Authorization\Model\Role setTreeLevel(int $value) + * @method Role setTreeLevel(int $value) * @method int getSortOrder() - * @method \Magento\Authorization\Model\Role setSortOrder(int $value) + * @method Role setSortOrder(int $value) * @method string getRoleType() - * @method \Magento\Authorization\Model\Role setRoleType(string $value) + * @method Role setRoleType(string $value) * @method int getUserId() - * @method \Magento\Authorization\Model\Role setUserId(int $value) + * @method Role setUserId(int $value) * @method string getUserType() - * @method \Magento\Authorization\Model\Role setUserType(string $value) + * @method Role setUserType(string $value) * @method string getRoleName() - * @method \Magento\Authorization\Model\Role setRoleName(string $value) + * @method Role setRoleName(string $value) * @api * @since 100.0.2 */ -class Role extends \Magento\Framework\Model\AbstractModel +class Role extends AbstractModel { /** * @var string @@ -38,23 +42,6 @@ class Role extends \Magento\Framework\Model\AbstractModel */ protected $_cacheTag = 'user_assigned_role'; - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Authorization\Model\ResourceModel\Role $resource - * @param \Magento\Authorization\Model\ResourceModel\Role\Collection $resourceCollection - * @param array $data - */ - public function __construct( //phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod - \Magento\Framework\Model\Context $context, - \Magento\Framework\Registry $registry, - \Magento\Authorization\Model\ResourceModel\Role $resource, - \Magento\Authorization\Model\ResourceModel\Role\Collection $resourceCollection, - array $data = [] - ) { - parent::__construct($context, $registry, $resource, $resourceCollection, $data); - } - /** * @inheritDoc */ @@ -70,31 +57,30 @@ public function __sleep() public function __wakeup() { parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $this->_resource = $objectManager->get(\Magento\Authorization\Model\ResourceModel\Role::class); - $this->_resourceCollection = $objectManager->get( - \Magento\Authorization\Model\ResourceModel\Role\Collection::class - ); + $objectManager = ObjectManager::getInstance(); + $this->_resource = $objectManager->get(ResourceModel\Role::class); + $this->_resourceCollection = $objectManager->get(Collection::class); } /** - * Class constructor - * - * @return void + * @inheritdoc */ protected function _construct() { - $this->_init(\Magento\Authorization\Model\ResourceModel\Role::class); + $this->_init(ResourceModel\Role::class); } /** - * Update object into database + * Obsolete method of update * * @return $this + * @deprecated Method was never implemented and used. */ public function update() { - $this->getResource()->update($this); + // phpcs:disable Magento2.Functions.DiscouragedFunction + trigger_error('Method was never implemented and used.', E_USER_DEPRECATED); + return $this; } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_methods.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_methods.php index d6a4053448fc8..892cd5d8e2f62 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_methods.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_methods.php @@ -2570,4 +2570,5 @@ ], ['isOrderIncrementIdUsed', 'Magento\Quote\Model\ResourceModel\Quote', 'Magento\Sales\Model\OrderIncrementIdChecker::isIncrementIdUsed'], ['update', 'Magento\Authorization\Model\Rules', 'Magento\Authorization\Model\Rules::update'], + ['update', 'Magento\Authorization\Model\Role', 'Magento\Authorization\Model\Role::update'], ]; From 1a843afbfc4d05483fd341158f12b4563b4d897b Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Thu, 18 Feb 2021 20:21:04 -0600 Subject: [PATCH 042/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Magento/Framework/App/Router/Base.php | 12 +- .../App/Test/Unit/Router/BaseTest.php | 172 ++++++++++++------ 2 files changed, 120 insertions(+), 64 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Router/Base.php b/lib/internal/Magento/Framework/App/Router/Base.php index 3c2945d5dbe08..665e3710b24bd 100644 --- a/lib/internal/Magento/Framework/App/Router/Base.php +++ b/lib/internal/Magento/Framework/App/Router/Base.php @@ -202,15 +202,16 @@ protected function matchModuleFrontName(\Magento\Framework\App\RequestInterface // get module name if ($request->getModuleName()) { $moduleFrontName = $request->getModuleName(); - } elseif (!empty($param)) { + } elseif (strlen($param)) { $moduleFrontName = $param; } else { $moduleFrontName = $this->_defaultPath->getPart('module'); $request->setAlias(\Magento\Framework\Url::REWRITE_REQUEST_PATH_ALIAS, ''); + if (!$moduleFrontName) { + return null; + } } - if (!$moduleFrontName) { - return null; - } + return $moduleFrontName; } @@ -270,7 +271,7 @@ protected function getNotFoundAction($currentModuleName) protected function matchAction(\Magento\Framework\App\RequestInterface $request, array $params) { $moduleFrontName = $this->matchModuleFrontName($request, $params['moduleFrontName']); - if (empty($moduleFrontName)) { + if (!strlen($moduleFrontName)) { return null; } @@ -278,7 +279,6 @@ protected function matchAction(\Magento\Framework\App\RequestInterface $request, * Searching router args by module name from route using it as key */ $modules = $this->_routeConfig->getModulesByFrontName($moduleFrontName); - if (empty($modules) === true) { return null; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php index a03a3675b8738..8a963780624d8 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php @@ -98,17 +98,17 @@ protected function setUp(): void * @param MockObject|Http $requestMock * @param string $defaultPath * @param string $moduleFrontName - * @param string $actionPath - * @param string $actionName - * @param string $moduleName + * @param string|null $actionPath + * @param string|null $actionName + * @param string|null $moduleName */ public function testMatch( MockObject $requestMock, string $defaultPath, string $moduleFrontName, - string $actionPath, - string $actionName, - string $moduleName + ?string $actionPath, + ?string $actionName, + ?string $moduleName ) { $actionInstance = 'Magento_TestFramework_ActionInstance'; @@ -153,13 +153,14 @@ public function matchDataProvider(): array $actionPath = 'action_path'; $actionName = 'action_name'; $moduleName = 'module_name'; - $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; $requestMock = $this->createMock(Http::class); $requestMock->expects($this->atLeastOnce())->method('getModuleName')->willReturn($moduleFrontName); $requestMock->expects($this->atLeastOnce())->method('getControllerName')->willReturn($actionPath); $requestMock->expects($this->atLeastOnce())->method('getActionName')->willReturn($actionName); - $requestMock->expects($this->atLeastOnce())->method('getPathInfo')->willReturn($paramList); + $requestMock->expects($this->atLeastOnce()) + ->method('getPathInfo') + ->willReturn($moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'); $emptyRequestMock = $this->createMock(Http::class); $emptyRequestMock->expects($this->atLeastOnce())->method('getModuleName')->willReturn(''); @@ -167,73 +168,128 @@ public function matchDataProvider(): array $emptyRequestMock->expects($this->atLeastOnce())->method('getActionName')->willReturn(''); $emptyRequestMock->expects($this->atLeastOnce())->method('getPathInfo')->willReturn(''); - $emptyRequestMock2 = clone $emptyRequestMock; - $emptyRequestMock2->expects($this->once())->method('getOriginalPathInfo')->willReturn(''); + $emptyRequestMock2 = $this->createMock(Http::class); + $emptyRequestMock2->expects($this->atLeastOnce())->method('getModuleName')->willReturn(''); + $emptyRequestMock2->expects($this->atLeastOnce())->method('getControllerName')->willReturn(''); + $emptyRequestMock2->expects($this->atLeastOnce())->method('getActionName')->willReturn(''); + $emptyRequestMock2->expects($this->atLeastOnce())->method('getPathInfo')->willReturn(''); + $emptyRequestMock2->expects($this->atLeastOnce())->method('getOriginalPathInfo')->willReturn(''); return [ - [$requestMock, '', $moduleFrontName, $actionPath, $actionName, $moduleName], - [$emptyRequestMock, $paramList, $moduleFrontName, $actionPath, $actionName, $moduleName], - [$emptyRequestMock2, '', $moduleFrontName, $actionPath, $actionName, $moduleName], + [ + $requestMock, + 'val1/val2/val3/', + $moduleFrontName, + $actionPath, + $actionName, + $moduleName + ], + [ + $emptyRequestMock, + $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/', + $moduleFrontName, + $actionPath, + $actionName, + $moduleName + ], + [ + $emptyRequestMock2, + '', + $moduleFrontName, + $actionPath, + $actionName, + $moduleName + ], ]; } - public function testMatchEmptyModuleList() - { - $moduleFrontName = 'module front name'; - $actionPath = 'action path'; - $actionName = 'action name'; - $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; + /** + * @dataProvider matchEmptyActionDataProvider + * @param MockObject|Http $requestMock + * @param string $defaultPath + * @param string $moduleFrontName + * @param string|null $actionPath + * @param string|null $actionName + * @param string|null $moduleName + */ + public function testMatchEmptyAction( + MockObject $requestMock, + string $defaultPath, + string $moduleFrontName, + ?string $actionPath, + ?string $actionName, + ?string $moduleName + ) { + $defaultReturnMap = [ + ['module', $moduleFrontName], + ['controller', $actionPath], + ['action', $actionName], + ]; + $this->defaultPathMock->method('getPart') + ->willReturnMap($defaultReturnMap); + $this->pathConfigMock->method('getDefaultPath') + ->willReturn($defaultPath); - $requestMock = $this->createMock(Http::class); - $requestMock->expects($this->atLeastOnce()) - ->method('getModuleName') - ->willReturn($moduleFrontName); - $requestMock->expects($this->atLeastOnce()) - ->method('getPathInfo') - ->willReturn($paramList); $this->routeConfigMock->expects($this->once()) ->method('getModulesByFrontName') ->with($moduleFrontName) - ->willReturn([]); - $this->actionListMock->expects($this->never())->method('get'); - $this->actionFactoryMock->expects($this->never())->method('create'); + ->willReturn($moduleName ? [$moduleName] : []); + $this->actionFactoryMock->expects($this->never()) + ->method('create'); $this->assertNull($this->model->match($requestMock)); } - public function testMatchEmptyActionInstance() + public function matchEmptyActionDataProvider(): array { - $moduleFrontName = 'module front name'; - $actionPath = 'action path'; - $actionName = 'action name'; - $moduleName = 'module name'; - $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; + $moduleFrontName = 'module_front_name'; + $actionPath = 'action_path'; + $actionName = 'action_name'; - $requestMock = $this->createMock(Http::class); - $requestMock->expects($this->atLeastOnce()) - ->method('getModuleName') - ->willReturn($moduleFrontName); - $requestMock->expects($this->atLeastOnce()) + $requestMock1 = $this->createMock(Http::class); + $requestMock1->expects($this->atLeastOnce())->method('getModuleName')->willReturn($moduleFrontName); + $requestMock1->expects($this->atLeastOnce())->method('getControllerName')->willReturn($actionPath); + $requestMock1->expects($this->atLeastOnce())->method('getActionName')->willReturn($actionName); + $requestMock1->expects($this->atLeastOnce()) ->method('getPathInfo') - ->willReturn($paramList); - $requestMock->expects($this->atLeastOnce()) - ->method('getControllerName') - ->willReturn($actionPath); - $requestMock->expects($this->once()) - ->method('getActionName') - ->willReturn($actionName); - $this->routeConfigMock->expects($this->once()) - ->method('getModulesByFrontName') - ->with($moduleFrontName) - ->willReturn([$moduleName]); - $this->actionListMock->expects($this->once()) - ->method('get') - ->with($moduleName) - ->willReturn(null); - $this->actionFactoryMock->expects($this->never()) - ->method('create'); + ->willReturn($moduleFrontName . '/' . $actionPath . '/' . $actionName . '/'); - $this->assertNull($this->model->match($requestMock)); + $requestMock2 = $this->createMock(Http::class); + $requestMock2->expects($this->atLeastOnce())->method('getModuleName')->willReturn($moduleFrontName); + $requestMock2->expects($this->atLeastOnce()) + ->method('getPathInfo') + ->willReturn($moduleFrontName . '/' . $actionPath . '/' . $actionName . '/'); + + $requestMock3 = $this->createMock(Http::class); + $requestMock3->expects($this->atLeastOnce())->method('getModuleName')->willReturn(''); + $requestMock3->expects($this->atLeastOnce())->method('getPathInfo')->willReturn('0'); + + return [ + [ + $requestMock1, + '', + $moduleFrontName, + $actionPath, + $actionName, + 'module_name', + ], + [ + $requestMock2, + '', + $moduleFrontName, + $actionPath, + $actionName, + null, + ], + [ + $requestMock3, + $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/', + '0', + null, + null, + null + ], + ]; } public function testGetActionClassName() From ad6f83483f2d8093e5fc03297dc8ef062368edad Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Thu, 18 Feb 2021 23:06:29 -0600 Subject: [PATCH 043/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- lib/internal/Magento/Framework/App/Router/Base.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Router/Base.php b/lib/internal/Magento/Framework/App/Router/Base.php index 665e3710b24bd..38db2df359c09 100644 --- a/lib/internal/Magento/Framework/App/Router/Base.php +++ b/lib/internal/Magento/Framework/App/Router/Base.php @@ -202,7 +202,7 @@ protected function matchModuleFrontName(\Magento\Framework\App\RequestInterface // get module name if ($request->getModuleName()) { $moduleFrontName = $request->getModuleName(); - } elseif (strlen($param)) { + } elseif (strlen((string) $param)) { $moduleFrontName = $param; } else { $moduleFrontName = $this->_defaultPath->getPart('module'); @@ -271,7 +271,7 @@ protected function getNotFoundAction($currentModuleName) protected function matchAction(\Magento\Framework\App\RequestInterface $request, array $params) { $moduleFrontName = $this->matchModuleFrontName($request, $params['moduleFrontName']); - if (!strlen($moduleFrontName)) { + if (!strlen((string) $moduleFrontName)) { return null; } From ec464f5e65022602b14dbf790466d7a6ee205937 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 19 Feb 2021 11:46:36 +0200 Subject: [PATCH 044/137] moved removing subscriber queue to separate class --- .../Plugin/RemoveSubscriberFromQueue.php | 18 ++++---- .../Model/RemoveSubscriberFromQueueLink.php | 44 +++++++++++++++++++ .../Newsletter/Model/ResourceModel/Queue.php | 14 ------ 3 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php diff --git a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php index 62437a35ce7a8..492d32b2f26c9 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php +++ b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php @@ -7,27 +7,25 @@ namespace Magento\Newsletter\Model\Plugin; -use Magento\Newsletter\Model\ResourceModel\Queue as QueueResource; +use Magento\Newsletter\Model\RemoveSubscriberFromQueueLink; use Magento\Newsletter\Model\Subscriber; /** - * Plugin responsible for removing subscriber from queue after unsubscribe + * Plugin for removing subscriber from queue after unsubscribe */ class RemoveSubscriberFromQueue { - private const STATUS = 'subscriber_status'; - /** - * @var QueueResource + * @var RemoveSubscriberFromQueueLink */ - private $queueResource; + private $removeSubscriberFromQueue; /** - * @param QueueResource $queueResource + * @param RemoveSubscriberFromQueueLink $removeSubscriberFromQueue */ - public function __construct(QueueResource $queueResource) + public function __construct(RemoveSubscriberFromQueueLink $removeSubscriberFromQueue) { - $this->queueResource = $queueResource; + $this->removeSubscriberFromQueue = $removeSubscriberFromQueue; } /** @@ -41,7 +39,7 @@ public function __construct(QueueResource $queueResource) public function afterUnsubscribe(Subscriber $subject, Subscriber $subscriber): Subscriber { if ($subscriber->isStatusChanged() && $subscriber->getSubscriberStatus() === Subscriber::STATUS_UNSUBSCRIBED) { - $this->queueResource->removeSubscriberFromQueue((int) $subscriber->getId()); + $this->removeSubscriberFromQueue->execute((int) $subscriber->getId()); } return $subscriber; diff --git a/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php b/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php new file mode 100644 index 0000000000000..6e11891e42ee2 --- /dev/null +++ b/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; + +/** + * Responsible for removing subscriber from queue + */ +class RemoveSubscriberFromQueueLink +{ + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource->getConnection(); + } + + /** + * Removes subscriber from queue + * + * @param int $subscriberId + * @return void + */ + public function execute(int $subscriberId): void + { + $this->connection->delete( + $this->connection->getTableName('newsletter_queue_link'), + ['subscriber_id = ?' => $subscriberId, 'letter_sent_at IS NULL'] + ); + } +} diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php index f327729c12018..476ce38fab107 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue.php @@ -125,20 +125,6 @@ public function removeSubscribersFromQueue(ModelQueue $queue) } } - /** - * Removes subscriber from queue - * - * @param int $subscriberId - * @return void - */ - public function removeSubscriberFromQueue(int $subscriberId): void - { - $this->getConnection()->delete( - $this->getTable('newsletter_queue_link'), - ['subscriber_id = ?' => $subscriberId, 'letter_sent_at IS NULL'] - ); - } - /** * Links queue to store * From 64066c41903c9c0fb20f75e527a1e8c5505fc9b5 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 19 Feb 2021 11:51:52 +0200 Subject: [PATCH 045/137] improved variable name --- .../Model/Plugin/RemoveSubscriberFromQueue.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php index 492d32b2f26c9..93b79744c01e9 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php +++ b/app/code/Magento/Newsletter/Model/Plugin/RemoveSubscriberFromQueue.php @@ -18,14 +18,14 @@ class RemoveSubscriberFromQueue /** * @var RemoveSubscriberFromQueueLink */ - private $removeSubscriberFromQueue; + private $removeSubscriberFromQueueLink; /** - * @param RemoveSubscriberFromQueueLink $removeSubscriberFromQueue + * @param RemoveSubscriberFromQueueLink $removeSubscriberFromQueueLink */ - public function __construct(RemoveSubscriberFromQueueLink $removeSubscriberFromQueue) + public function __construct(RemoveSubscriberFromQueueLink $removeSubscriberFromQueueLink) { - $this->removeSubscriberFromQueue = $removeSubscriberFromQueue; + $this->removeSubscriberFromQueueLink = $removeSubscriberFromQueueLink; } /** @@ -39,7 +39,7 @@ public function __construct(RemoveSubscriberFromQueueLink $removeSubscriberFromQ public function afterUnsubscribe(Subscriber $subject, Subscriber $subscriber): Subscriber { if ($subscriber->isStatusChanged() && $subscriber->getSubscriberStatus() === Subscriber::STATUS_UNSUBSCRIBED) { - $this->removeSubscriberFromQueue->execute((int) $subscriber->getId()); + $this->removeSubscriberFromQueueLink->execute((int) $subscriber->getId()); } return $subscriber; From ee503112e228fd641ca5224bce4781c38bb0e2b3 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Fri, 19 Feb 2021 12:46:40 +0000 Subject: [PATCH 046/137] Added scope back to preserve backward compatibility since getRegionJson is public --- app/code/Magento/Directory/Helper/Data.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index b2fd3971ecc8b..b359989fe981b 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -422,6 +422,11 @@ private function getCurrentScope(): array 'type' => ScopeInterface::SCOPE_WEBSITE, 'value' => $request->getParam(ScopeInterface::SCOPE_WEBSITE), ]; + } elseif ($request->getParam(ScopeInterface::SCOPE_STORE)) { + $scope = [ + 'type' => ScopeInterface::SCOPE_STORE, + 'value' => $request->getParam(ScopeInterface::SCOPE_STORE), + ]; } elseif ($request->getParam(self::STORE_ID)) { $scope = [ 'type' => ScopeInterface::SCOPE_STORE, From df8b398d9c283eaa049112c1473863eb57d2b656 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Fri, 19 Feb 2021 18:57:23 -0600 Subject: [PATCH 047/137] MC-40788: Customer is unable to convert all consumers to ampq - added test --- .../MessageQueue/ConfigGetConsumersTest.php | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Framework/MessageQueue/ConfigGetConsumersTest.php diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/ConfigGetConsumersTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/ConfigGetConsumersTest.php new file mode 100644 index 0000000000000..f2a548890b524 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/ConfigGetConsumersTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\MessageQueue; + +use Magento\Framework\App\DeploymentConfig\FileReader; +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Filesystem; +use Magento\Framework\MessageQueue\Config; +use Magento\Framework\MessageQueue\Config\Data; +use Magento\Framework\MessageQueue\Config\Reader\Xml; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class ConfigGetConsumersTest extends TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + /** + * @var Config + */ + private $configSubject; + + /** + * @var FileReader + */ + private $fileReader; + /** + * @var array + */ + private $envConfigBackup; + + /** + * @var Writer + */ + private $fileWriter; + + /** + * @var Xml + */ + private $xmlReader; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->fileWriter = $this->objectManager->get(Writer::class); + $this->xmlReader = $this->objectManager->create(Xml::class); + $this->fileReader = $this->objectManager->get(FileReader::class); + + $this->envConfigBackup = $this->fileReader->load(ConfigFilePool::APP_ENV); + $customEnvConfig = $this->buildCustomEnvConfigWithConsumers(); + $this->fileWriter->saveConfig([ConfigFilePool::APP_ENV => $customEnvConfig]); + + /** @var Data data */ + $configData = $this->objectManager->create( + Data::class, + [ + 'cacheId' => uniqid(microtime()) + ] + ); + + $this->configSubject = $this->objectManager->create( + Config::class, + [ + 'queueConfigData' => $configData + ] + ); + } + + public function testGetConsumers(): void + { + $consumers = $this->configSubject->getConsumers(); + + foreach ($consumers as $consumer) { + $this->assertIsString($consumer['name']); + $this->assertIsArray($consumer['handlers']); + } + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $filesystem = $this->objectManager->get(Filesystem::class); + $configFilePool = $this->objectManager->get(ConfigFilePool::class); + $filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $configFilePool->getPath(ConfigFilePool::APP_ENV), + "<?php\n return array();\n" + ); + $this->fileWriter->saveConfig([ConfigFilePool::APP_ENV => $this->envConfigBackup]); + } + + private function buildCustomEnvConfigWithConsumers(): array + { + $data = $this->xmlReader->read(); + $names = array_keys($data['consumers']); + $consumers = []; + foreach ($names as $name) { + $consumers[$name] = ['connection' => 'amqp']; + } + + return [ + 'queue' => [ + 'amqp' => [ + 'host' => 'localhost', + 'port' => '5672', + 'user' => 'guest', + 'password' => 'guest', + 'virtualhost' => '/', + 'ssl' => '' + ], + 'consumers' => $consumers + ], + ]; + } +} From f3c3a1db9c04c7dc811d8518ff7413465fc1ad1d Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Mon, 22 Feb 2021 13:47:35 +0200 Subject: [PATCH 048/137] used resourceConnection --- .../Model/RemoveSubscriberFromQueueLink.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php b/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php index 6e11891e42ee2..6f741ce719bfc 100644 --- a/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php +++ b/app/code/Magento/Newsletter/Model/RemoveSubscriberFromQueueLink.php @@ -8,7 +8,6 @@ namespace Magento\Newsletter\Model; use Magento\Framework\App\ResourceConnection; -use Magento\Framework\DB\Adapter\AdapterInterface; /** * Responsible for removing subscriber from queue @@ -16,16 +15,16 @@ class RemoveSubscriberFromQueueLink { /** - * @var AdapterInterface + * @var ResourceConnection */ - private $connection; + private $resourceConnection; /** - * @param ResourceConnection $resource + * @param ResourceConnection $resourceConnection */ - public function __construct(ResourceConnection $resource) + public function __construct(ResourceConnection $resourceConnection) { - $this->connection = $resource->getConnection(); + $this->resourceConnection = $resourceConnection; } /** @@ -36,8 +35,10 @@ public function __construct(ResourceConnection $resource) */ public function execute(int $subscriberId): void { - $this->connection->delete( - $this->connection->getTableName('newsletter_queue_link'), + $connection = $this->resourceConnection->getConnection(); + + $connection->delete( + $this->resourceConnection->getTableName('newsletter_queue_link'), ['subscriber_id = ?' => $subscriberId, 'letter_sent_at IS NULL'] ); } From 81ab4fb8e14a4afb873ab21d51d986068f56eedb Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 22 Feb 2021 15:55:20 +0200 Subject: [PATCH 049/137] MC-40832: Create automated test for: "Verify that customer attribute label is displayed according saved values for each store view" --- .../Customer/Block/Address/EditTest.php | 31 ++++++++++++++++++- ...attribute_postcode_store_label_address.php | 26 ++++++++++++++++ ..._postcode_store_label_address_rollback.php | 21 +++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php index 12585992d084c..4617ea40e4325 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php @@ -16,6 +16,7 @@ use Magento\Framework\View\Result\PageFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** @@ -23,6 +24,8 @@ * * @magentoAppArea frontend * @magentoAppIsolation enabled + * + * @magentoDataFixture Magento/Customer/_files/customer.php */ class EditTest extends TestCase { @@ -41,9 +44,12 @@ class EditTest extends TestCase /** @var CustomerRegistry */ private $customerRegistry; - /** @var RequestInterface */ + /** @var RequestInterface */ private $request; + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -62,6 +68,7 @@ protected function setUp(): void $this->block = $page->getLayout()->getBlock('customer_address_edit'); $this->addressRegistry = $this->objectManager->get(AddressRegistry::class); $this->customerRegistry = $this->objectManager->get(CustomerRegistry::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); } /** @@ -174,4 +181,26 @@ public function testVatIdFieldNotVisible(): void $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']"; $this->assertEquals(0, Xpath::getElementsCountForXpath($inputXpath, $html)); } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/attribute_postcode_store_label_address.php + * + * @return void + */ + public function testCheckPostCodeLabels(): void + { + $newLabel = 'default store postcode label'; + $html = $this->executeInStoreContext->execute('default', [$this->block, 'toHtml']); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//form[contains(@class, 'form-address-edit')]//label[@for='zip']/span[contains(text(), '%s')]", + $newLabel + ), + $html + ) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address.php new file mode 100644 index 0000000000000..769e64491eab9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$attribute = $attributeRepository->get(AddressMetadataInterface::ENTITY_TYPE_ADDRESS, AddressInterface::POSTCODE); +$storeLabels = $attribute->getStoreLabels(); +$stores = $storeManager->getStores(); +foreach ($stores as $store) { + $storeLabels[$store->getId()] = $store->getCode() . ' store postcode label'; +} +$attribute->setStoreLabels($storeLabels); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php new file mode 100644 index 0000000000000..65e8acd6e6a0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$attribute = $attributeRepository->get(AddressMetadataInterface::ENTITY_TYPE_ADDRESS, AddressInterface::POSTCODE); +$attribute->setStoreLabels([]); +$attributeRepository->save($attribute); From ea92f9b4770e77bee733b6825244f193f6d53fd6 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Mon, 22 Feb 2021 12:20:17 -0600 Subject: [PATCH 050/137] MC-38539: Introduce JWT wrapper --- .../Model/AlgorithmProviderFactory.php | 25 +++++++++++++++++++ .../Model/JweAlgorithmManagerFactory.php | 12 +++++++-- .../Model/JweCompressionManagerFactory.php | 16 ++++++++++-- .../JweContentAlgorithmManagerFactory.php | 12 +++++++-- .../Model/JwsAlgorithmManagerFactory.php | 11 +++++++- .../Magento/JwtFrameworkAdapter/etc/di.xml | 7 ++++++ 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/JwtFrameworkAdapter/Model/AlgorithmProviderFactory.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/AlgorithmProviderFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/AlgorithmProviderFactory.php new file mode 100644 index 0000000000000..365fa25fbd361 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Model/AlgorithmProviderFactory.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Model; + +use Jose\Easy\AlgorithmProvider; + +class AlgorithmProviderFactory +{ + /** + * Create provider instance. + * + * @param string[] $algorithms Algorithm classes. + * @return AlgorithmProvider + */ + public function create(array $algorithms): AlgorithmProvider + { + return new AlgorithmProvider($algorithms); + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php index 7ae4643396bae..a1a53db3e450b 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php @@ -9,7 +9,6 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Core\AlgorithmManager; -use Jose\Easy\AlgorithmProvider; class JweAlgorithmManagerFactory { @@ -32,8 +31,17 @@ class JweAlgorithmManagerFactory \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS512A256KW::class ]; + /** + * @var AlgorithmProviderFactory + */ + private $algorithmProviderFactory; + + public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) { + $this->algorithmProviderFactory = $algorithmProviderFactory; + } + public function create(): AlgorithmManager { - return new AlgorithmManager((new AlgorithmProvider(self::ALGOS))->getAvailableAlgorithms()); + return new AlgorithmManager($this->algorithmProviderFactory->create(self::ALGOS)->getAvailableAlgorithms()); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php index 16367cff6a534..af7a8d982be70 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php @@ -9,12 +9,24 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Encryption\Compression\CompressionMethodManager; -use Jose\Component\Encryption\Compression\Deflate; class JweCompressionManagerFactory { + /** + * @var \Jose\Component\Encryption\Compression\CompressionMethod[] + */ + private $methods; + + /** + * @param \Jose\Component\Encryption\Compression\CompressionMethod[] $methods + */ + public function __construct(array $methods) + { + $this->methods = $methods; + } + public function create(): CompressionMethodManager { - return new CompressionMethodManager([new Deflate()]); + return new CompressionMethodManager($this->methods); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php index 7c5db44b4fd9b..ded1e63fabf27 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php @@ -9,7 +9,6 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Core\AlgorithmManager; -use Jose\Easy\AlgorithmProvider; class JweContentAlgorithmManagerFactory { @@ -22,8 +21,17 @@ class JweContentAlgorithmManagerFactory \Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM::class, ]; + /** + * @var AlgorithmProviderFactory + */ + private $algorithmProviderFactory; + + public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) { + $this->algorithmProviderFactory = $algorithmProviderFactory; + } + public function create(): AlgorithmManager { - return new AlgorithmManager((new AlgorithmProvider(self::ALGOS))->getAvailableAlgorithms()); + return new AlgorithmManager($this->algorithmProviderFactory->create(self::ALGOS)->getAvailableAlgorithms()); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php index a4a5c702b5864..e9478727b5597 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php @@ -31,8 +31,17 @@ class JwsAlgorithmManagerFactory \Jose\Component\Signature\Algorithm\None::class ]; + /** + * @var AlgorithmProviderFactory + */ + private $algorithmProviderFactory; + + public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) { + $this->algorithmProviderFactory = $algorithmProviderFactory; + } + public function create(): AlgorithmManager { - return new AlgorithmManager((new AlgorithmProvider(self::ALGOS))->getAvailableAlgorithms()); + return new AlgorithmManager($this->algorithmProviderFactory->create(self::ALGOS)->getAvailableAlgorithms()); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/etc/di.xml b/app/code/Magento/JwtFrameworkAdapter/etc/di.xml index 2a8248c67c9b7..c7234f28b8b0f 100644 --- a/app/code/Magento/JwtFrameworkAdapter/etc/di.xml +++ b/app/code/Magento/JwtFrameworkAdapter/etc/di.xml @@ -7,4 +7,11 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Framework\Jwt\JwtManagerInterface" type="Magento\JwtFrameworkAdapter\Model\JwtManager" /> + <type name="Magento\JwtFrameworkAdapter\Model\JweCompressionManagerFactory"> + <arguments> + <argument name="methods" xsi:type="array"> + <item name="deflate" xsi:type="object">Jose\Component\Encryption\Compression\Deflate</item> + </argument> + </arguments> + </type> </config> From 7cdf8bd21dfe99e7392362b3fbea0a957cc53354 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 22 Feb 2021 20:09:24 -0600 Subject: [PATCH 051/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Store/App/Request/PathInfoProcessor.php | 44 +++++------- .../App/Request/StorePathInfoValidator.php | 71 +++++++++---------- app/code/Magento/Store/Model/Store.php | 35 ++++----- .../Model/Validation/StoreCodeValidator.php | 47 ++++++++++++ .../Model/Validation/StoreNameValidator.php | 41 +++++++++++ .../Store/Model/Validation/StoreValidator.php | 50 +++++++++++++ app/code/Magento/Store/etc/di.xml | 8 +++ 7 files changed, 210 insertions(+), 86 deletions(-) create mode 100644 app/code/Magento/Store/Model/Validation/StoreCodeValidator.php create mode 100644 app/code/Magento/Store/Model/Validation/StoreNameValidator.php create mode 100644 app/code/Magento/Store/Model/Validation/StoreValidator.php diff --git a/app/code/Magento/Store/App/Request/PathInfoProcessor.php b/app/code/Magento/Store/App/Request/PathInfoProcessor.php index fad0d07c3a0a7..23a0ca898bfbd 100644 --- a/app/code/Magento/Store/App/Request/PathInfoProcessor.php +++ b/app/code/Magento/Store/App/Request/PathInfoProcessor.php @@ -7,10 +7,13 @@ namespace Magento\Store\App\Request; +use Magento\Framework\App\Request\PathInfoProcessorInterface; +use Magento\Framework\App\RequestInterface; + /** * Processes the path and looks for the store in the url and removes it and modifies the path accordingly. */ -class PathInfoProcessor implements \Magento\Framework\App\Request\PathInfoProcessorInterface +class PathInfoProcessor implements PathInfoProcessorInterface { /** * @var StorePathInfoValidator @@ -18,20 +21,11 @@ class PathInfoProcessor implements \Magento\Framework\App\Request\PathInfoProces private $storePathInfoValidator; /** - * @var \Magento\Framework\App\Config\ReinitableConfigInterface - */ - private $config; - - /** - * @param \Magento\Store\App\Request\StorePathInfoValidator $storePathInfoValidator - * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config + * @param StorePathInfoValidator $storePathInfoValidator */ - public function __construct( - \Magento\Store\App\Request\StorePathInfoValidator $storePathInfoValidator, - \Magento\Framework\App\Config\ReinitableConfigInterface $config - ) { + public function __construct(StorePathInfoValidator $storePathInfoValidator) + { $this->storePathInfoValidator = $storePathInfoValidator; - $this->config = $config; } /** @@ -39,24 +33,22 @@ public function __construct( * * This method also sets request to no route if store is not valid and store is present in url config is enabled * - * @param \Magento\Framework\App\RequestInterface $request + * @param RequestInterface $request * @param string $pathInfo * @return string */ - public function process(\Magento\Framework\App\RequestInterface $request, $pathInfo) : string + public function process(RequestInterface $request, $pathInfo) : string { - //can store code be used in url - if ((bool)$this->config->getValue(\Magento\Store\Model\Store::XML_PATH_STORE_IN_URL)) { - $storeCode = $this->storePathInfoValidator->getValidStoreCode($request, $pathInfo); - if (!empty($storeCode)) { - if (!$request->isDirectAccessFrontendName($storeCode)) { - $pathInfo = $this->trimStoreCodeFromPathInfo($pathInfo, $storeCode); - } else { - //no route in case we're trying to access a store that has the same code as a direct access - $request->setActionName(\Magento\Framework\App\Router\Base::NO_ROUTE); - } + $storeCode = $this->storePathInfoValidator->getValidStoreCode($request, $pathInfo); + if (!empty($storeCode)) { + if (!$request->isDirectAccessFrontendName($storeCode)) { + $pathInfo = $this->trimStoreCodeFromPathInfo($pathInfo, $storeCode); + } else { + //no route in case we're trying to access a store that has the same code as a direct access + $request->setActionName(\Magento\Framework\App\Router\Base::NO_ROUTE); } } + return $pathInfo; } @@ -67,7 +59,7 @@ public function process(\Magento\Framework\App\RequestInterface $request, $pathI * @param string $storeCode * @return string */ - private function trimStoreCodeFromPathInfo(string $pathInfo, string $storeCode) : ?string + private function trimStoreCodeFromPathInfo(string $pathInfo, string $storeCode) : string { if (substr($pathInfo, 0, strlen('/' . $storeCode)) == '/'. $storeCode) { $pathInfo = substr($pathInfo, strlen($storeCode)+1); diff --git a/app/code/Magento/Store/App/Request/StorePathInfoValidator.php b/app/code/Magento/Store/App/Request/StorePathInfoValidator.php index 0b66ba7586009..f9715f8812cb5 100644 --- a/app/code/Magento/Store/App/Request/StorePathInfoValidator.php +++ b/app/code/Magento/Store/App/Request/StorePathInfoValidator.php @@ -7,8 +7,13 @@ namespace Magento\Store\App\Request; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Request\PathInfo; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreIsInactiveException; /** * Gets the store from the path if valid @@ -18,29 +23,29 @@ class StorePathInfoValidator /** * Store Config * - * @var \Magento\Framework\App\Config\ReinitableConfigInterface + * @var ScopeConfigInterface */ private $config; /** - * @var \Magento\Store\Api\StoreRepositoryInterface + * @var StoreRepositoryInterface */ private $storeRepository; /** - * @var \Magento\Framework\App\Request\PathInfo + * @var PathInfo */ private $pathInfo; /** - * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config - * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository - * @param \Magento\Framework\App\Request\PathInfo $pathInfo + * @param ScopeConfigInterface $config + * @param StoreRepositoryInterface $storeRepository + * @param PathInfo $pathInfo */ public function __construct( - \Magento\Framework\App\Config\ReinitableConfigInterface $config, - \Magento\Store\Api\StoreRepositoryInterface $storeRepository, - \Magento\Framework\App\Request\PathInfo $pathInfo + ScopeConfigInterface $config, + StoreRepositoryInterface $storeRepository, + PathInfo $pathInfo ) { $this->config = $config; $this->storeRepository = $storeRepository; @@ -50,42 +55,34 @@ public function __construct( /** * Get store code from path info validate if config value. If path info is empty the try to calculate from request. * - * @param \Magento\Framework\App\Request\Http $request + * @param Http $request * @param string $pathInfo * @return string|null */ - public function getValidStoreCode( - \Magento\Framework\App\Request\Http $request, - string $pathInfo = '' - ) : ?string { + public function getValidStoreCode(Http $request, string $pathInfo = '') : ?string + { + $useStoreCodeInUrl = (bool) $this->config->getValue(Store::XML_PATH_STORE_IN_URL); + if (!$useStoreCodeInUrl) { + return null; + } + if (empty($pathInfo)) { - $pathInfo = $this->pathInfo->getPathInfo( - $request->getRequestUri(), - $request->getBaseUrl() - ); + $pathInfo = $this->pathInfo->getPathInfo($request->getRequestUri(), $request->getBaseUrl()); } $storeCode = $this->getStoreCode($pathInfo); - if (!empty($storeCode) - && $storeCode != Store::ADMIN_CODE - && (bool)$this->config->getValue(\Magento\Store\Model\Store::XML_PATH_STORE_IN_URL) - ) { - try { - $this->storeRepository->getActiveStoreByCode($storeCode); + if (empty($storeCode) || $storeCode === Store::ADMIN_CODE) { + return null; + } + + try { + $this->storeRepository->getActiveStoreByCode($storeCode); - if ((bool)$this->config->getValue( - \Magento\Store\Model\Store::XML_PATH_STORE_IN_URL, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeCode - )) { - return $storeCode; - } - } catch (NoSuchEntityException $e) { - //return null; - } catch (\Magento\Store\Model\StoreIsInactiveException $e) { - //return null; - } + return $storeCode; + } catch (NoSuchEntityException $e) { + return null; + } catch (StoreIsInactiveException $e) { + return null; } - return null; } /** diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 7bcb3282ba552..82bd68b9a5fe6 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -332,6 +332,11 @@ class Store extends AbstractExtensibleModel implements */ private $pillPut; + /** + * @var Validation\StoreValidator + */ + private $modelValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -359,7 +364,7 @@ class Store extends AbstractExtensibleModel implements * @param array $data optional generic object data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut - * + * @param \Magento\Store\Model\Validation\StoreValidator|null $modelValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -388,7 +393,8 @@ public function __construct( $isCustomEntryPoint = false, array $data = [], \Magento\Framework\Event\ManagerInterface $eventManager = null, - \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null, + ?\Magento\Store\Model\Validation\StoreValidator $modelValidator = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -411,6 +417,8 @@ public function __construct( ->get(\Magento\Framework\Event\ManagerInterface::class); $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); + $this->modelValidator = $modelValidator ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Store\Model\Validation\StoreValidator::class); parent::__construct( $context, $registry, @@ -472,30 +480,11 @@ protected function _getSession() } /** - * Validation rules for store - * - * @return \Zend_Validate_Interface|null - * @throws \Zend_Validate_Exception + * @inheritDoc */ protected function _getValidationRulesBeforeSave() { - $validator = new \Magento\Framework\Validator\DataObject(); - - $storeLabelRule = new \Zend_Validate_NotEmpty(); - $storeLabelRule->setMessage(__('Name is required'), \Zend_Validate_NotEmpty::IS_EMPTY); - $validator->addRule($storeLabelRule, 'name'); - - $storeCodeRule = new \Zend_Validate_Regex('/^[a-z]+[a-z0-9_]*$/i'); - $storeCodeRule->setMessage( - __( - 'The store code may contain only letters (a-z), numbers (0-9) or underscore (_),' - . ' and the first character must be a letter.' - ), - \Zend_Validate_Regex::NOT_MATCH - ); - $validator->addRule($storeCodeRule, 'code'); - - return $validator; + return $this->modelValidator; } /** diff --git a/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php b/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php new file mode 100644 index 0000000000000..7db120ee2a47f --- /dev/null +++ b/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Validation; + +use Magento\Framework\Validator\AbstractValidator; +use Magento\Framework\Validator\RegexFactory; + +/** + * Validator for store code. + */ +class StoreCodeValidator extends AbstractValidator +{ + /** + * @var RegexFactory + */ + private $regexValidatorFactory; + + /** + * @param RegexFactory $regexValidatorFactory + */ + public function __construct(RegexFactory $regexValidatorFactory) + { + $this->regexValidatorFactory = $regexValidatorFactory; + } + + /** + * @inheritDoc + */ + public function isValid($value) + { + $validator = $this->regexValidatorFactory->create(['pattern' => '/^[a-z]+[a-z0-9_]*$/i']); + $validator->setMessage( + __( + 'The store code may contain only letters (a-z), numbers (0-9) or underscore (_),' + . ' and the first character must be a letter.' + ), + \Zend_Validate_Regex::NOT_MATCH + ); + + return $validator->isValid($value); + } +} diff --git a/app/code/Magento/Store/Model/Validation/StoreNameValidator.php b/app/code/Magento/Store/Model/Validation/StoreNameValidator.php new file mode 100644 index 0000000000000..4bef3421da502 --- /dev/null +++ b/app/code/Magento/Store/Model/Validation/StoreNameValidator.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Validation; + +use Magento\Framework\Validator\AbstractValidator; +use Magento\Framework\Validator\NotEmptyFactory; + +/** + * Validator for store name. + */ +class StoreNameValidator extends AbstractValidator +{ + /** + * @var NotEmptyFactory + */ + private $notEmptyValidatorFactory; + + /** + * @param NotEmptyFactory $notEmptyValidatorFactory + */ + public function __construct(NotEmptyFactory $notEmptyValidatorFactory) + { + $this->notEmptyValidatorFactory = $notEmptyValidatorFactory; + } + + /** + * @inheritDoc + */ + public function isValid($value) + { + $validator = $this->notEmptyValidatorFactory->create(); + $validator->setMessage(__('Name is required'), \Zend_Validate_NotEmpty::IS_EMPTY); + + return $validator->isValid($value); + } +} diff --git a/app/code/Magento/Store/Model/Validation/StoreValidator.php b/app/code/Magento/Store/Model/Validation/StoreValidator.php new file mode 100644 index 0000000000000..edebf043ef091 --- /dev/null +++ b/app/code/Magento/Store/Model/Validation/StoreValidator.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Validation; + +use Magento\Framework\Validator\AbstractValidator; +use Magento\Framework\Validator\DataObjectFactory; + +/** + * Store model validator. + */ +class StoreValidator extends AbstractValidator +{ + /** + * @var DataObjectFactory + */ + private $dataObjectValidatorFactory; + + /** + * @var array + */ + private $rules; + + /** + * @param DataObjectFactory $dataObjectValidatorFactory + * @param array $rules + */ + public function __construct(DataObjectFactory $dataObjectValidatorFactory, array $rules) + { + $this->dataObjectValidatorFactory = $dataObjectValidatorFactory; + $this->rules = $rules; + } + + /** + * @inheritDoc + */ + public function isValid($value) + { + $validator = $this->dataObjectValidatorFactory->create(); + foreach ($this->rules as $fieldName => $rule) { + $validator->addRule($rule, $fieldName); + } + + return $validator->isValid($value); + } +} diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index ea1df9293bc92..a51cb300cd80d 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -444,4 +444,12 @@ </argument> </arguments> </type> + <type name="Magento\Store\Model\Validation\StoreValidator"> + <arguments> + <argument name="rules" xsi:type="array"> + <item name="name" xsi:type="object">Magento\Store\Model\Validation\StoreNameValidator</item> + <item name="code" xsi:type="object">Magento\Store\Model\Validation\StoreCodeValidator</item> + </argument> + </arguments> + </type> </config> From 2ceeea4d8682f3d25ed8e10ac5456fa1571fd87a Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 22 Feb 2021 20:47:19 -0600 Subject: [PATCH 052/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Model/Address/AbstractAddress.php | 9 +++++--- .../Magento/Quote/Model/Quote/Address.php | 19 +++++++---------- .../App/Request/StorePathInfoValidator.php | 13 ++++++++++-- app/code/Magento/Store/Model/Store.php | 21 ++++--------------- app/code/Magento/Store/etc/di.xml | 1 + .../Magento/Tax/Model/Calculation/Rule.php | 17 ++------------- .../Model/AbstractExtensibleModel.php | 6 ++++-- .../Magento/Framework/Model/AbstractModel.php | 12 +++++++++-- 8 files changed, 46 insertions(+), 52 deletions(-) diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index d1364dc0aeba6..d4d877af6df81 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -140,7 +140,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param CompositeValidator $compositeValidator - * + * @param \Magento\Framework\Validator\ValidatorInterface|null $modelValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -160,7 +160,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - CompositeValidator $compositeValidator = null + CompositeValidator $compositeValidator = null, + ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null ) { $this->_directoryData = $directoryData; $data = $this->_implodeArrayField($data); @@ -174,6 +175,7 @@ public function __construct( $this->dataObjectHelper = $dataObjectHelper; $this->compositeValidator = $compositeValidator ?: ObjectManager::getInstance() ->get(CompositeValidator::class); + parent::__construct( $context, $registry, @@ -181,7 +183,8 @@ public function __construct( $customAttributeFactory, $resource, $resourceCollection, - $data + $data, + $modelValidator ); } diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 6a06deb9cbbef..f191a0b72f31a 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\Data\AddressInterfaceFactory; use Magento\Customer\Api\Data\RegionInterfaceFactory; use Magento\Customer\Model\Address\AbstractAddress; +use Magento\Customer\Model\Address\CompositeValidator; use Magento\Customer\Model\Address\Mapper; use Magento\Directory\Helper\Data; use Magento\Directory\Model\CountryFactory; @@ -334,7 +335,7 @@ class Address extends AbstractAddress implements * @param array $data * @param Json $serializer * @param StoreManagerInterface $storeManager - * + * @param CompositeValidator $compositeValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -371,7 +372,8 @@ public function __construct( AbstractDb $resourceCollection = null, array $data = [], Json $serializer = null, - StoreManagerInterface $storeManager = null + StoreManagerInterface $storeManager = null, + CompositeValidator $compositeValidator = null ) { $this->_scopeConfig = $scopeConfig; $this->_addressItemFactory = $addressItemFactory; @@ -392,6 +394,7 @@ public function __construct( $this->totalsReader = $totalsReader; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + parent::__construct( $context, $registry, @@ -408,7 +411,9 @@ public function __construct( $dataObjectHelper, $resource, $resourceCollection, - $data + $data, + $compositeValidator, + $this->validator ); } @@ -1422,14 +1427,6 @@ public function getAllBaseTotalAmounts() /******************************* End Total Collector Interface *******************************************/ - /** - * @inheritdoc - */ - protected function _getValidationRulesBeforeSave() - { - return $this->validator; - } - /** * @inheritdoc */ diff --git a/app/code/Magento/Store/App/Request/StorePathInfoValidator.php b/app/code/Magento/Store/App/Request/StorePathInfoValidator.php index f9715f8812cb5..abbf29fd0c916 100644 --- a/app/code/Magento/Store/App/Request/StorePathInfoValidator.php +++ b/app/code/Magento/Store/App/Request/StorePathInfoValidator.php @@ -14,6 +14,7 @@ use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreIsInactiveException; +use Magento\Store\Model\Validation\StoreCodeValidator; /** * Gets the store from the path if valid @@ -37,19 +38,27 @@ class StorePathInfoValidator */ private $pathInfo; + /** + * @var StoreCodeValidator + */ + private $storeCodeValidator; + /** * @param ScopeConfigInterface $config * @param StoreRepositoryInterface $storeRepository * @param PathInfo $pathInfo + * @param StoreCodeValidator $storeCodeValidator */ public function __construct( ScopeConfigInterface $config, StoreRepositoryInterface $storeRepository, - PathInfo $pathInfo + PathInfo $pathInfo, + StoreCodeValidator $storeCodeValidator ) { $this->config = $config; $this->storeRepository = $storeRepository; $this->pathInfo = $pathInfo; + $this->storeCodeValidator = $storeCodeValidator; } /** @@ -70,7 +79,7 @@ public function getValidStoreCode(Http $request, string $pathInfo = '') : ?strin $pathInfo = $this->pathInfo->getPathInfo($request->getRequestUri(), $request->getBaseUrl()); } $storeCode = $this->getStoreCode($pathInfo); - if (empty($storeCode) || $storeCode === Store::ADMIN_CODE) { + if (empty($storeCode) || $storeCode === Store::ADMIN_CODE || !$this->storeCodeValidator->isValid($storeCode)) { return null; } diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 82bd68b9a5fe6..07a64f8c279b2 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -332,11 +332,6 @@ class Store extends AbstractExtensibleModel implements */ private $pillPut; - /** - * @var Validation\StoreValidator - */ - private $modelValidator; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -394,7 +389,7 @@ public function __construct( array $data = [], \Magento\Framework\Event\ManagerInterface $eventManager = null, \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null, - ?\Magento\Store\Model\Validation\StoreValidator $modelValidator = null + ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -417,8 +412,7 @@ public function __construct( ->get(\Magento\Framework\Event\ManagerInterface::class); $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); - $this->modelValidator = $modelValidator ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Store\Model\Validation\StoreValidator::class); + parent::__construct( $context, $registry, @@ -426,7 +420,8 @@ public function __construct( $customAttributeFactory, $resource, $resourceCollection, - $data + $data, + $modelValidator ); } @@ -479,14 +474,6 @@ protected function _getSession() return $this->_session; } - /** - * @inheritDoc - */ - protected function _getValidationRulesBeforeSave() - { - return $this->modelValidator; - } - /** * Loading store data * diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index a51cb300cd80d..007805b3531df 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -97,6 +97,7 @@ <argument name="session" xsi:type="object" shared="false">Magento\Framework\Session\Generic\Proxy</argument> <argument name="isCustomEntryPoint" xsi:type="init_parameter">Magento\Store\Model\Store::CUSTOM_ENTRY_POINT_PARAM</argument> <argument name="url" xsi:type="object" shared="false">Magento\Framework\UrlInterface</argument> + <argument name="modelValidator" xsi:type="object">Magento\Store\Model\Validation\StoreValidator</argument> </arguments> </type> <type name="Magento\Store\Model\StoreManager"> diff --git a/app/code/Magento/Tax/Model/Calculation/Rule.php b/app/code/Magento/Tax/Model/Calculation/Rule.php index d8060590e849b..9e7e15811ccaf 100644 --- a/app/code/Magento/Tax/Model/Calculation/Rule.php +++ b/app/code/Magento/Tax/Model/Calculation/Rule.php @@ -43,11 +43,6 @@ class Rule extends \Magento\Framework\Model\AbstractExtensibleModel implements T */ protected $_calculation; - /** - * @var \Magento\Tax\Model\Calculation\Rule\Validator - */ - protected $validator; - /** * Name of object id field * @@ -81,7 +76,6 @@ public function __construct( array $data = [] ) { $this->_calculation = $calculation; - $this->validator = $validator; parent::__construct( $context, $registry, @@ -89,7 +83,8 @@ public function __construct( $customAttributeFactory, $resource, $resourceCollection, - $data + $data, + $validator ); $this->_init(\Magento\Tax\Model\ResourceModel\Calculation\Rule::class); $this->_taxClass = $taxClass; @@ -280,14 +275,6 @@ protected function _getUniqueValues($values) return array_values(array_unique($values)); } - /** - * {@inheritdoc} - */ - protected function _getValidationRulesBeforeSave() - { - return $this->validator; - } - /** * Set tax rule code * diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index 306159f8d22d5..57581f58977bc 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -55,6 +55,7 @@ abstract class AbstractExtensibleModel extends AbstractModel implements * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Framework\Validator\ValidatorInterface|null $modelValidator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -63,12 +64,13 @@ public function __construct( AttributeValueFactory $customAttributeFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null ) { $this->extensionAttributesFactory = $extensionFactory; $this->customAttributeFactory = $customAttributeFactory; $data = $this->filterCustomAttributes($data); - parent::__construct($context, $registry, $resource, $resourceCollection, $data); + parent::__construct($context, $registry, $resource, $resourceCollection, $data, $modelValidator); if (isset($data['id'])) { $this->setId($data['id']); } diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index b6473f8b0ab3c..de92999338aa9 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -163,19 +163,26 @@ abstract class AbstractModel extends \Magento\Framework\DataObject */ protected $storedData = []; + /** + * @var \Magento\Framework\Validator\ValidatorInterface|null + */ + private $modelValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Framework\Validator\ValidatorInterface|null $modelValidator */ public function __construct( \Magento\Framework\Model\Context $context, \Magento\Framework\Registry $registry, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null ) { $this->_registry = $registry; $this->_appState = $context->getAppState(); @@ -185,6 +192,7 @@ public function __construct( $this->_resourceCollection = $resourceCollection; $this->_logger = $context->getLogger(); $this->_actionValidator = $context->getActionValidator(); + $this->modelValidator = $modelValidator; if (method_exists($this->_resource, 'getIdFieldName') || $this->_resource instanceof \Magento\Framework\DataObject @@ -775,7 +783,7 @@ protected function _createValidatorBeforeSave() */ protected function _getValidationRulesBeforeSave() { - return null; + return $this->modelValidator; } /** From 668dd3d4697717624499f46426463248e7cf5837 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Tue, 23 Feb 2021 11:31:41 +0200 Subject: [PATCH 053/137] MC-23989: Mini cart missing when you edit inline welcome message for guest and use special characters --- .../Magento/Theme/view/frontend/templates/html/header.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml index cbefb82f23e33..63552dc052b48 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml @@ -18,7 +18,7 @@ $welcomeMessage = $block->getWelcome(); <!-- /ko --> <!-- ko ifnot: customer().fullname --> <span class="not-logged-in" - data-bind='html:"<?= $block->escapeHtml($welcomeMessage) ?>"'></span> + data-bind="html: '<?= $block->escapeHtml($welcomeMessage) ?>'"></span> <?= $block->getBlockHtml('header.additional') ?> <!-- /ko --> </li> From 33f29e03bbc66fbd627639f179bb29283dbe50a4 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 23 Feb 2021 11:55:51 +0200 Subject: [PATCH 054/137] MC-40832: Create automated test for: "Verify that customer attribute label is displayed according saved values for each store view" --- .../Magento/Customer/Block/Address/EditTest.php | 5 +++-- .../attribute_postcode_store_label_address_rollback.php | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php index 4617ea40e4325..c48e3f9a0175b 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php @@ -22,6 +22,8 @@ /** * Tests Address Edit Block * + * @see \Magento\Customer\Block\Address\Edit + * * @magentoAppArea frontend * @magentoAppIsolation enabled * @@ -190,14 +192,13 @@ public function testVatIdFieldNotVisible(): void */ public function testCheckPostCodeLabels(): void { - $newLabel = 'default store postcode label'; $html = $this->executeInStoreContext->execute('default', [$this->block, 'toHtml']); $this->assertEquals( 1, Xpath::getElementsCountForXpath( sprintf( "//form[contains(@class, 'form-address-edit')]//label[@for='zip']/span[contains(text(), '%s')]", - $newLabel + 'default store postcode label' ), $html ) diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php index 65e8acd6e6a0e..60cb5bb671d21 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_postcode_store_label_address_rollback.php @@ -8,14 +8,23 @@ use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); +$registry = $objectManager->get(Registry::class); /** @var AttributeRepositoryInterface $attributeRepository */ $attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); /** @var StoreManagerInterface $storeManager */ $storeManager = $objectManager->get(StoreManagerInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + $attribute = $attributeRepository->get(AddressMetadataInterface::ENTITY_TYPE_ADDRESS, AddressInterface::POSTCODE); $attribute->setStoreLabels([]); $attributeRepository->save($attribute); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 3a384b9e4e28fed0f68c2e9e27240357665e8905 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 23 Feb 2021 14:55:55 +0200 Subject: [PATCH 055/137] MC-40865: Create automated test for: ""Date and Time" attribute correctly rendered regarding timezone settings" --- .../Attribute/Frontend/DatetimeTest.php | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php new file mode 100644 index 0000000000000..a18119b4e911a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\Entity\Attribute\Frontend; + +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; + +/** + * Checks Datetime attribute's frontend model + * + * @see \Magento\Eav\Model\Entity\Attribute\Frontend\Datetime + */ +class DatetimeTest extends TestCase +{ + /** + * @var int + */ + private const ONE_HOUR_IN_MILLISECONDS = 3600; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var DateTime + */ + private $dateTime; + + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->attributeRepository = $this->objectManager->get(ProductAttributeRepositoryInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->dateTime = $this->objectManager->create(DateTime::class); + } + + /** + * @magentoDbIsolation disabled + * + * @magentoDataFixture Magento/Catalog/_files/product_two_websites.php + * @magentoDataFixture Magento/Catalog/_files/product_datetime_attribute.php + * + * @magentoConfigFixture default_store general/locale/timezone Europe/Moscow + * @magentoConfigFixture fixture_second_store_store general/locale/timezone Europe/Kiev + * + * @return void + */ + public function testFrontendValueOnDifferentWebsites(): void + { + $attribute = $this->attributeRepository->get('datetime_attribute'); + $product = $this->productRepository->get('simple-on-two-websites'); + $product->setDatetimeAttribute($this->dateTime->date('Y-m-d H:i:s')); + $valueOnWebsiteOne = $attribute->getFrontend()->getValue($product); + $secondStoreId = $this->storeManager->getStore('fixture_second_store')->getId(); + $this->storeManager->setCurrentStore($secondStoreId); + $valueOnWebsiteTwo = $attribute->getFrontend()->getValue($product); + $this->assertEquals( + self::ONE_HOUR_IN_MILLISECONDS, + $this->dateTime->gmtTimestamp($valueOnWebsiteOne) - $this->dateTime->gmtTimestamp($valueOnWebsiteTwo), + 'The difference between the two time zones are incorrect' + ); + } +} From f5004d319c3da1408ab30261c2f24e8663729ec5 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Tue, 23 Feb 2021 15:24:25 +0200 Subject: [PATCH 056/137] MC-23989: Mini cart missing when you edit inline welcome message for guest and use special characters --- .../Magento/Theme/view/frontend/templates/html/header.phtml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml index 63552dc052b48..3075e903bc174 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml @@ -6,6 +6,7 @@ /** * @var \Magento\Theme\Block\Html\Header $block + * @var \Magento\Framework\Escaper $escaper */ $welcomeMessage = $block->getWelcome(); ?> @@ -13,12 +14,12 @@ $welcomeMessage = $block->getWelcome(); <li class="greet welcome" data-bind="scope: 'customer'"> <!-- ko if: customer().fullname --> <span class="logged-in" - data-bind="text: new String('<?= $block->escapeHtml(__('Welcome, %1!', '%1')) ?>').replace('%1', customer().fullname)"> + data-bind="text: new String('<?= $escaper->escapeHtml(__('Welcome, %1!', '%1')) ?>').replace('%1', customer().fullname)"> </span> <!-- /ko --> <!-- ko ifnot: customer().fullname --> <span class="not-logged-in" - data-bind="html: '<?= $block->escapeHtml($welcomeMessage) ?>'"></span> + data-bind="html: "<?= $escaper->escapeHtml($welcomeMessage) ?>"></span> <?= $block->getBlockHtml('header.additional') ?> <!-- /ko --> </li> From 4fb151c18e7a8dec9369fa326e562db4b53684a1 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 23 Feb 2021 09:28:04 -0600 Subject: [PATCH 057/137] MC-38539: Introduce JWT wrapper --- .../Model/JweCompressionManagerFactory.php | 16 ++-------------- app/code/Magento/JwtFrameworkAdapter/etc/di.xml | 7 ------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php index af7a8d982be70..16367cff6a534 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweCompressionManagerFactory.php @@ -9,24 +9,12 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Encryption\Compression\CompressionMethodManager; +use Jose\Component\Encryption\Compression\Deflate; class JweCompressionManagerFactory { - /** - * @var \Jose\Component\Encryption\Compression\CompressionMethod[] - */ - private $methods; - - /** - * @param \Jose\Component\Encryption\Compression\CompressionMethod[] $methods - */ - public function __construct(array $methods) - { - $this->methods = $methods; - } - public function create(): CompressionMethodManager { - return new CompressionMethodManager($this->methods); + return new CompressionMethodManager([new Deflate()]); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/etc/di.xml b/app/code/Magento/JwtFrameworkAdapter/etc/di.xml index c7234f28b8b0f..2a8248c67c9b7 100644 --- a/app/code/Magento/JwtFrameworkAdapter/etc/di.xml +++ b/app/code/Magento/JwtFrameworkAdapter/etc/di.xml @@ -7,11 +7,4 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Framework\Jwt\JwtManagerInterface" type="Magento\JwtFrameworkAdapter\Model\JwtManager" /> - <type name="Magento\JwtFrameworkAdapter\Model\JweCompressionManagerFactory"> - <arguments> - <argument name="methods" xsi:type="array"> - <item name="deflate" xsi:type="object">Jose\Component\Encryption\Compression\Deflate</item> - </argument> - </arguments> - </type> </config> From f9ebaca23b8dd5f2585008a4467527d68cd82933 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 23 Feb 2021 09:46:25 -0600 Subject: [PATCH 058/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Model/Address/AbstractAddress.php | 8 +++---- .../Magento/Quote/Model/Quote/Address.php | 19 ++++++++++------- app/code/Magento/Store/Model/Store.php | 21 ++++++++++++++++--- app/code/Magento/Store/etc/di.xml | 1 - .../Magento/Tax/Model/Calculation/Rule.php | 17 +++++++++++++-- .../Model/AbstractExtensibleModel.php | 6 ++---- .../Magento/Framework/Model/AbstractModel.php | 12 ++--------- 7 files changed, 51 insertions(+), 33 deletions(-) diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index d4d877af6df81..2033429836893 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -140,7 +140,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param CompositeValidator $compositeValidator - * @param \Magento\Framework\Validator\ValidatorInterface|null $modelValidator + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -160,8 +160,7 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - CompositeValidator $compositeValidator = null, - ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null + CompositeValidator $compositeValidator = null ) { $this->_directoryData = $directoryData; $data = $this->_implodeArrayField($data); @@ -183,8 +182,7 @@ public function __construct( $customAttributeFactory, $resource, $resourceCollection, - $data, - $modelValidator + $data ); } diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index f191a0b72f31a..6a06deb9cbbef 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -10,7 +10,6 @@ use Magento\Customer\Api\Data\AddressInterfaceFactory; use Magento\Customer\Api\Data\RegionInterfaceFactory; use Magento\Customer\Model\Address\AbstractAddress; -use Magento\Customer\Model\Address\CompositeValidator; use Magento\Customer\Model\Address\Mapper; use Magento\Directory\Helper\Data; use Magento\Directory\Model\CountryFactory; @@ -335,7 +334,7 @@ class Address extends AbstractAddress implements * @param array $data * @param Json $serializer * @param StoreManagerInterface $storeManager - * @param CompositeValidator $compositeValidator + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -372,8 +371,7 @@ public function __construct( AbstractDb $resourceCollection = null, array $data = [], Json $serializer = null, - StoreManagerInterface $storeManager = null, - CompositeValidator $compositeValidator = null + StoreManagerInterface $storeManager = null ) { $this->_scopeConfig = $scopeConfig; $this->_addressItemFactory = $addressItemFactory; @@ -394,7 +392,6 @@ public function __construct( $this->totalsReader = $totalsReader; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); - parent::__construct( $context, $registry, @@ -411,9 +408,7 @@ public function __construct( $dataObjectHelper, $resource, $resourceCollection, - $data, - $compositeValidator, - $this->validator + $data ); } @@ -1427,6 +1422,14 @@ public function getAllBaseTotalAmounts() /******************************* End Total Collector Interface *******************************************/ + /** + * @inheritdoc + */ + protected function _getValidationRulesBeforeSave() + { + return $this->validator; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 07a64f8c279b2..1a6c23b183288 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -332,6 +332,11 @@ class Store extends AbstractExtensibleModel implements */ private $pillPut; + /** + * @var \Magento\Store\Model\Validation\StoreValidator + */ + private $modelValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -360,6 +365,7 @@ class Store extends AbstractExtensibleModel implements * @param \Magento\Framework\Event\ManagerInterface|null $eventManager * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut * @param \Magento\Store\Model\Validation\StoreValidator|null $modelValidator + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -389,7 +395,7 @@ public function __construct( array $data = [], \Magento\Framework\Event\ManagerInterface $eventManager = null, \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null, - ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null + \Magento\Store\Model\Validation\StoreValidator $modelValidator = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -412,6 +418,8 @@ public function __construct( ->get(\Magento\Framework\Event\ManagerInterface::class); $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); + $this->modelValidator = $modelValidator ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Store\Model\Validation\StoreValidator::class); parent::__construct( $context, @@ -420,8 +428,7 @@ public function __construct( $customAttributeFactory, $resource, $resourceCollection, - $data, - $modelValidator + $data ); } @@ -474,6 +481,14 @@ protected function _getSession() return $this->_session; } + /** + * @inheritDoc + */ + protected function _getValidationRulesBeforeSave() + { + return $this->modelValidator; + } + /** * Loading store data * diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 007805b3531df..a51cb300cd80d 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -97,7 +97,6 @@ <argument name="session" xsi:type="object" shared="false">Magento\Framework\Session\Generic\Proxy</argument> <argument name="isCustomEntryPoint" xsi:type="init_parameter">Magento\Store\Model\Store::CUSTOM_ENTRY_POINT_PARAM</argument> <argument name="url" xsi:type="object" shared="false">Magento\Framework\UrlInterface</argument> - <argument name="modelValidator" xsi:type="object">Magento\Store\Model\Validation\StoreValidator</argument> </arguments> </type> <type name="Magento\Store\Model\StoreManager"> diff --git a/app/code/Magento/Tax/Model/Calculation/Rule.php b/app/code/Magento/Tax/Model/Calculation/Rule.php index 9e7e15811ccaf..d8060590e849b 100644 --- a/app/code/Magento/Tax/Model/Calculation/Rule.php +++ b/app/code/Magento/Tax/Model/Calculation/Rule.php @@ -43,6 +43,11 @@ class Rule extends \Magento\Framework\Model\AbstractExtensibleModel implements T */ protected $_calculation; + /** + * @var \Magento\Tax\Model\Calculation\Rule\Validator + */ + protected $validator; + /** * Name of object id field * @@ -76,6 +81,7 @@ public function __construct( array $data = [] ) { $this->_calculation = $calculation; + $this->validator = $validator; parent::__construct( $context, $registry, @@ -83,8 +89,7 @@ public function __construct( $customAttributeFactory, $resource, $resourceCollection, - $data, - $validator + $data ); $this->_init(\Magento\Tax\Model\ResourceModel\Calculation\Rule::class); $this->_taxClass = $taxClass; @@ -275,6 +280,14 @@ protected function _getUniqueValues($values) return array_values(array_unique($values)); } + /** + * {@inheritdoc} + */ + protected function _getValidationRulesBeforeSave() + { + return $this->validator; + } + /** * Set tax rule code * diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index 57581f58977bc..306159f8d22d5 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -55,7 +55,6 @@ abstract class AbstractExtensibleModel extends AbstractModel implements * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param \Magento\Framework\Validator\ValidatorInterface|null $modelValidator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -64,13 +63,12 @@ public function __construct( AttributeValueFactory $customAttributeFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], - ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null + array $data = [] ) { $this->extensionAttributesFactory = $extensionFactory; $this->customAttributeFactory = $customAttributeFactory; $data = $this->filterCustomAttributes($data); - parent::__construct($context, $registry, $resource, $resourceCollection, $data, $modelValidator); + parent::__construct($context, $registry, $resource, $resourceCollection, $data); if (isset($data['id'])) { $this->setId($data['id']); } diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index de92999338aa9..b6473f8b0ab3c 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -163,26 +163,19 @@ abstract class AbstractModel extends \Magento\Framework\DataObject */ protected $storedData = []; - /** - * @var \Magento\Framework\Validator\ValidatorInterface|null - */ - private $modelValidator; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param \Magento\Framework\Validator\ValidatorInterface|null $modelValidator */ public function __construct( \Magento\Framework\Model\Context $context, \Magento\Framework\Registry $registry, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], - ?\Magento\Framework\Validator\ValidatorInterface $modelValidator = null + array $data = [] ) { $this->_registry = $registry; $this->_appState = $context->getAppState(); @@ -192,7 +185,6 @@ public function __construct( $this->_resourceCollection = $resourceCollection; $this->_logger = $context->getLogger(); $this->_actionValidator = $context->getActionValidator(); - $this->modelValidator = $modelValidator; if (method_exists($this->_resource, 'getIdFieldName') || $this->_resource instanceof \Magento\Framework\DataObject @@ -783,7 +775,7 @@ protected function _createValidatorBeforeSave() */ protected function _getValidationRulesBeforeSave() { - return $this->modelValidator; + return null; } /** From 95819f2b448c58edc38468efbfaa7b02a775f87b Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 23 Feb 2021 14:42:23 -0600 Subject: [PATCH 059/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Model/Address/AbstractAddress.php | 1 - app/code/Magento/Store/Model/Store.php | 5 +- .../Model/Validation/StoreNameValidator.php | 2 +- .../App/Request/PathInfoProcessorTest.php | 175 +++++++++--------- .../App/Request/PathInfoProcessorTest.php | 45 ----- 5 files changed, 89 insertions(+), 139 deletions(-) diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index 2033429836893..d1364dc0aeba6 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -174,7 +174,6 @@ public function __construct( $this->dataObjectHelper = $dataObjectHelper; $this->compositeValidator = $compositeValidator ?: ObjectManager::getInstance() ->get(CompositeValidator::class); - parent::__construct( $context, $registry, diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 1a6c23b183288..f437d9bca0b74 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -482,7 +482,10 @@ protected function _getSession() } /** - * @inheritDoc + * Validation rules for store + * + * @return \Zend_Validate_Interface|null + * @throws \Zend_Validate_Exception */ protected function _getValidationRulesBeforeSave() { diff --git a/app/code/Magento/Store/Model/Validation/StoreNameValidator.php b/app/code/Magento/Store/Model/Validation/StoreNameValidator.php index 4bef3421da502..b03b25d6d6d3d 100644 --- a/app/code/Magento/Store/Model/Validation/StoreNameValidator.php +++ b/app/code/Magento/Store/Model/Validation/StoreNameValidator.php @@ -33,7 +33,7 @@ public function __construct(NotEmptyFactory $notEmptyValidatorFactory) */ public function isValid($value) { - $validator = $this->notEmptyValidatorFactory->create(); + $validator = $this->notEmptyValidatorFactory->create(['options' => []]); $validator->setMessage(__('Name is required'), \Zend_Validate_NotEmpty::IS_EMPTY); return $validator->isValid($value); diff --git a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php index c6b8225530089..93735031d2444 100644 --- a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php @@ -7,7 +7,7 @@ namespace Magento\Store\Test\Unit\App\Request; -use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; use Magento\Framework\App\Request\PathInfo; use Magento\Framework\Exception\NoSuchEntityException; @@ -15,6 +15,7 @@ use Magento\Store\App\Request\PathInfoProcessor; use Magento\Store\App\Request\StorePathInfoValidator; use Magento\Store\Model\Store; +use Magento\Store\Model\Validation\StoreCodeValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,152 +27,144 @@ class PathInfoProcessorTest extends TestCase private $model; /** - * @var MockObject + * @var MockObject|Http */ private $requestMock; /** - * @var MockObject + * @var MockObject|ScopeConfigInterface */ private $validatorConfigMock; /** - * @var MockObject + * @var MockObject|PathInfo */ - private $processorConfigMock; + private $pathInfoMock; /** - * @var MockObject + * @var MockObject|StoreCodeValidator */ - private $pathInfoMock; + private $storeCodeValidator; /** - * @var MockObject + * @var MockObject|StoreRepositoryInterface */ private $storeRepositoryMock; /** - * @var MockObject + * @var StorePathInfoValidator */ private $storePathInfoValidator; /** * @var string */ - protected $pathInfo = '/storeCode/node_one/'; + private $pathInfo = '/storeCode/node_one/'; protected function setUp(): void { - $this->requestMock = $this->getMockBuilder(Http::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->validatorConfigMock = $this->getMockForAbstractClass(ReinitableConfigInterface::class); - - $this->processorConfigMock = $this->getMockForAbstractClass(ReinitableConfigInterface::class); - - $this->storeRepositoryMock = $this->getMockForAbstractClass(StoreRepositoryInterface::class); + $this->requestMock = $this->createMock(Http::class); - $this->pathInfoMock = $this->getMockBuilder(PathInfo ::class) - ->disableOriginalConstructor() - ->getMock(); + $this->validatorConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->storeRepositoryMock = $this->createMock(StoreRepositoryInterface::class); + $this->pathInfoMock = $this->createMock(PathInfo ::class); + $this->storeCodeValidator = $this->createMock(StoreCodeValidator::class); $this->storePathInfoValidator = new StorePathInfoValidator( $this->validatorConfigMock, $this->storeRepositoryMock, - $this->pathInfoMock + $this->pathInfoMock, + $this->storeCodeValidator ); - $this->model = new PathInfoProcessor( - $this->storePathInfoValidator, - $this->validatorConfigMock + $this->storePathInfoValidator ); } public function testProcessIfStoreExistsAndIsNotDirectAccessToFrontName() { - $this->validatorConfigMock->expects($this->any())->method('getValue')->willReturn(true); + $this->validatorConfigMock->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(true); + $this->storeCodeValidator->expects($this->atLeastOnce()) + ->method('isValid') + ->willReturn(true); $store = $this->createMock(Store::class); - $this->storeRepositoryMock->expects( - $this->atLeastOnce() - )->method( - 'getActiveStoreByCode' - )->with( - 'storeCode' - )->willReturn($store); - $this->requestMock->expects( - $this->atLeastOnce() - )->method( - 'isDirectAccessFrontendName' - )->with( - 'storeCode' - )->willReturn( - false - ); - $this->assertEquals('/node_one/', $this->model->process($this->requestMock, $this->pathInfo)); + $this->storeRepositoryMock->expects($this->once()) + ->method('getActiveStoreByCode') + ->with('storeCode') + ->willReturn($store); + $this->requestMock->expects($this->atLeastOnce()) + ->method('isDirectAccessFrontendName') + ->with('storeCode') + ->willReturn(false); + + $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); + $this->assertEquals('/node_one/', $pathInfo); } public function testProcessIfStoreExistsAndDirectAccessToFrontName() { - $this->validatorConfigMock->expects($this->atLeastOnce())->method('getValue')->willReturn(true); - - $this->storeRepositoryMock->expects( - $this->any() - )->method( - 'getActiveStoreByCode' - ); - $this->requestMock->expects( - $this->atLeastOnce() - )->method( - 'isDirectAccessFrontendName' - )->with( - 'storeCode' - )->willReturn(true); - $this->requestMock->expects($this->once())->method('setActionName')->with('noroute'); - $this->assertEquals($this->pathInfo, $this->model->process($this->requestMock, $this->pathInfo)); + $this->validatorConfigMock->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(true); + $this->storeCodeValidator->expects($this->atLeastOnce()) + ->method('isValid') + ->willReturn(true); + + $this->storeRepositoryMock->expects($this->once()) + ->method('getActiveStoreByCode'); + $this->requestMock->expects($this->atLeastOnce()) + ->method('isDirectAccessFrontendName') + ->with('storeCode') + ->willReturn(true); + $this->requestMock->expects($this->once()) + ->method('setActionName') + ->with('noroute'); + + $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); + $this->assertEquals($this->pathInfo, $pathInfo); } public function testProcessIfStoreIsEmpty() { - $this->validatorConfigMock->expects($this->any())->method('getValue')->willReturn(true); + $this->validatorConfigMock->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(true); + $this->storeCodeValidator->expects($this->any()) + ->method('isValid') + ->willReturn(true); $path = '/0/node_one/'; - $this->storeRepositoryMock->expects( - $this->never() - )->method( - 'getActiveStoreByCode' - ); - $this->requestMock->expects( - $this->never() - )->method( - 'isDirectAccessFrontendName' - ); - $this->requestMock->expects($this->never())->method('setActionName'); - $this->assertEquals($path, $this->model->process($this->requestMock, $path)); + $this->storeRepositoryMock->expects($this->never()) + ->method('getActiveStoreByCode'); + $this->requestMock->expects($this->never()) + ->method('isDirectAccessFrontendName'); + $this->requestMock->expects($this->never()) + ->method('setActionName'); + + $pathInfo = $this->model->process($this->requestMock, $path); + $this->assertEquals($path, $pathInfo); } public function testProcessIfStoreCodeIsNotExist() { - $this->validatorConfigMock->expects($this->atLeastOnce())->method('getValue')->willReturn(true); - - $this->storeRepositoryMock->expects($this->once())->method('getActiveStoreByCode')->with('storeCode') + $this->validatorConfigMock->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(true); + $this->storeCodeValidator->expects($this->atLeastOnce()) + ->method('isValid') + ->willReturn(true); + + $this->storeRepositoryMock->expects($this->once()) + ->method('getActiveStoreByCode') + ->with('storeCode') ->willThrowException(new NoSuchEntityException()); - $this->requestMock->expects($this->never())->method('isDirectAccessFrontendName'); - - $this->assertEquals($this->pathInfo, $this->model->process($this->requestMock, $this->pathInfo)); - } - - public function testProcessIfStoreUrlNotEnabled() - { - $this->validatorConfigMock->expects($this->at(0))->method('getValue')->willReturn(true); - - $this->validatorConfigMock->expects($this->at(1))->method('getValue')->willReturn(true); - - $this->validatorConfigMock->expects($this->at(2))->method('getValue')->willReturn(false); - - $this->storeRepositoryMock->expects($this->once())->method('getActiveStoreByCode')->willReturn(1); + $this->requestMock->expects($this->never()) + ->method('isDirectAccessFrontendName'); - $this->assertEquals($this->pathInfo, $this->model->process($this->requestMock, $this->pathInfo)); + $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); + $this->assertEquals($this->pathInfo, $pathInfo); } } diff --git a/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php b/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php index be3675a9fb53c..a7662dadac050 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php @@ -46,27 +46,6 @@ public function notValidStoreCodeDataProvider() ]; } - /** - * @covers \Magento\Store\App\Request\PathInfoProcessor::process - * @magentoDataFixture Magento/Store/_files/core_fixturestore.php - */ - public function testProcessValidStoreDisabledStoreUrl() - { - /** @var \Magento\Store\Model\Store $store */ - $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); - - /** @var \Magento\Framework\App\RequestInterface $request */ - $request = Bootstrap::getObjectManager()->create(\Magento\Framework\App\RequestInterface::class); - - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true); - $config->setValue(Store::XML_PATH_STORE_IN_URL, false, ScopeInterface::SCOPE_STORE, $store->getCode()); - $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); - $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); - } - /** * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php @@ -113,30 +92,6 @@ public function testProcessValidStoreCodeWhenStoreIsDirectFrontNameWithFrontName $this->assertEquals(\Magento\Framework\App\Router\Base::NO_ROUTE, $request->getActionName()); } - /** - * @covers \Magento\Store\App\Request\PathInfoProcessor::process - * @magentoDataFixture Magento/Store/_files/core_fixturestore.php - */ - public function testProcessValidStoreCodeWhenStoreCodeInUrlIsDisabledWithFrontName() - { - /** @var \Magento\Store\Model\Store $store */ - $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); - - /** @var \Magento\Framework\App\RequestInterface $request */ - $request = Bootstrap::getObjectManager()->create( - \Magento\Framework\App\RequestInterface::class, - ['directFrontNames' => ['someFrontName' => true]] - ); - - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true); - $config->setValue(Store::XML_PATH_STORE_IN_URL, false, ScopeInterface::SCOPE_STORE, $store->getCode()); - $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); - $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); - } - /** * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php From 709128432cb03d263b74f00a76ec5f6fca8b61ab Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Tue, 23 Feb 2021 14:54:57 -0600 Subject: [PATCH 060/137] MC-40817: Unable to open fullscreen video using mobile browsers --- lib/web/fotorama/fotorama.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index f268c9aa73667..38c26bf8f94bd 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -3739,7 +3739,8 @@ fotoramaVersion = '4.6.4'; } activeIndexes = []; - detachFrames(STAGE_FRAME_KEY); + + // detachFrames(STAGE_FRAME_KEY); reset.ok = true; From a8fc518b5737f8dd312ff0154a082e9d553af02f Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 23 Feb 2021 15:27:53 -0600 Subject: [PATCH 061/137] MC-38539: Introduce JWT wrapper --- .../Test/Unit/Model/JweFactoryTest.php | 131 ++++++++++++++++++ .../Test/Unit/Model/JwsFactoryTest.php | 118 ++++++++++++++++ .../Unit/Model/UnsecuredJwtFactoryTest.php | 120 ++++++++++++++++ .../Jwt/Test/Unit/Header/X509ChainTest.php | 29 ++++ .../Unit/Header/X509Sha1ThumbprintTest.php | 22 +++ .../Unit/Header/X509Sha256ThumbprintTest.php | 22 +++ .../Test/Unit/Jwe/JweEncryptionJwksTest.php | 114 +++++++++++++++ .../Test/Unit/Jws/JwsSignatureJwksTest.php | 112 +++++++++++++++ 8 files changed, 668 insertions(+) create mode 100644 app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JweFactoryTest.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JwsFactoryTest.php create mode 100644 app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/UnsecuredJwtFactoryTest.php create mode 100644 lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509ChainTest.php create mode 100644 lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha1ThumbprintTest.php create mode 100644 lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha256ThumbprintTest.php create mode 100644 lib/internal/Magento/Framework/Jwt/Test/Unit/Jwe/JweEncryptionJwksTest.php create mode 100644 lib/internal/Magento/Framework/Jwt/Test/Unit/Jws/JwsSignatureJwksTest.php diff --git a/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JweFactoryTest.php b/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JweFactoryTest.php new file mode 100644 index 0000000000000..1258aab828a0a --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JweFactoryTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Test\Unit\Model; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\JwtFrameworkAdapter\Model\JweFactory; +use PHPUnit\Framework\TestCase; + +class JweFactoryTest extends TestCase +{ + /** + * @var JweFactory + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->model = new JweFactory(); + } + + public function getCreateCases(): array + { + return [ + 'compact-arbitrary' => [ + ['cty' => 'MyType', 'typ' => 'JWT'], + 'some-value', + null, + null, + ArbitraryPayload::class + ], + 'compact-claims' => [ + ['typ' => 'JWT'], + '{"tst1":"val1","tst2":2,"tst3":true}', + null, + null, + ClaimsPayloadInterface::class + ], + 'compact-nested' => [ + ['typ' => 'JWT', 'cty' => NestedPayloadInterface::CONTENT_TYPE], + 'eyJhbGciOiJub25lIn0.' + .'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.', + null, + null, + NestedPayloadInterface::class + ], + 'json-arbitrary' => [ + ['typ' => 'JWT'], + 'arbitrary', + ['cty' => 'SomeType'], + ['crit' => 'exp'], + ArbitraryPayload::class + ], + 'json-claims' => [ + ['typ' => 'JWT'], + '{"tst1":"val1","tst2":2,"tst3":true}', + ['aud' => 'magento'], + ['custom' => 'value'], + ClaimsPayloadInterface::class + ] + ]; + } + + /** + * Test "create" method. + * + * @param array $headers + * @param string $content + * @param array|null $unprotected + * @param array|null $perRecipient + * @param string $payloadClass + * @return void + * @dataProvider getCreateCases + */ + public function testCreate( + array $headers, + string $content, + ?array $unprotected, + ?array $perRecipient, + string $payloadClass + ): void { + $jwe = $this->model->create($headers, $content, $unprotected, $perRecipient); + + $payload = $jwe->getPayload(); + $this->assertEquals($content, $payload->getContent()); + $this->assertInstanceOf($payloadClass, $payload); + if ($payload instanceof ClaimsPayloadInterface) { + $actualClaims = []; + foreach ($payload->getClaims() as $claim) { + $actualClaims[$claim->getName()] = $claim->getValue(); + } + $this->assertEquals(json_decode($content, true), $actualClaims); + } + + $this->validateHeader($headers, $jwe->getHeader()); + if ($unprotected === null) { + $this->assertNull($jwe->getSharedUnprotectedHeader()); + } else { + $this->assertNotNull($jwe->getSharedUnprotectedHeader()); + $this->validateHeader($unprotected, $jwe->getSharedUnprotectedHeader()); + } + if ($perRecipient === null) { + $this->assertNull($jwe->getSharedUnprotectedHeader()); + } else { + $this->assertNotNull($jwe->getSharedUnprotectedHeader()); + $this->validateHeader($unprotected, $jwe->getSharedUnprotectedHeader()); + } + } + + private function validateHeader(array $expectedHeaders, HeaderInterface $actual): void + { + foreach ($expectedHeaders as $header => $value) { + $parameter = $actual->getParameter($header); + $this->assertNotNull($parameter); + $this->assertEquals($value, $parameter->getValue()); + } + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JwsFactoryTest.php b/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JwsFactoryTest.php new file mode 100644 index 0000000000000..addbf4c983832 --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/JwsFactoryTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Test\Unit\Model; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\JwtFrameworkAdapter\Model\JwsFactory; +use PHPUnit\Framework\TestCase; + +class JwsFactoryTest extends TestCase +{ + /** + * @var JwsFactory + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->model = new JwsFactory(); + } + + public function getCreateCases(): array + { + return [ + 'compact-arbitrary' => [ + ['cty' => 'MyType', 'typ' => 'JWT'], + 'some-value', + null, + ArbitraryPayload::class + ], + 'compact-claims' => [ + ['typ' => 'JWT'], + '{"tst1":"val1","tst2":2,"tst3":true}', + null, + ClaimsPayloadInterface::class + ], + 'compact-nested' => [ + ['typ' => 'JWT', 'cty' => NestedPayloadInterface::CONTENT_TYPE], + 'eyJhbGciOiJub25lIn0.' + .'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.', + null, + NestedPayloadInterface::class + ], + 'json-arbitrary' => [ + ['typ' => 'JWT'], + 'arbitrary', + ['cty' => 'SomeType'], + ArbitraryPayload::class + ], + 'json-claims' => [ + ['typ' => 'JWT'], + '{"tst1":"val1","tst2":2,"tst3":true}', + ['aud' => 'magento'], + ClaimsPayloadInterface::class + ] + ]; + } + + /** + * Test "create" method. + * + * @param array $headers + * @param string $content + * @param array|null $unprotected + * @param string $payloadClass + * @return void + * @dataProvider getCreateCases + */ + public function testCreate( + array $headers, + string $content, + ?array $unprotected, + string $payloadClass + ): void { + $jws = $this->model->create($headers, $content, $unprotected); + + $payload = $jws->getPayload(); + $this->assertEquals($content, $payload->getContent()); + $this->assertInstanceOf($payloadClass, $payload); + if ($payload instanceof ClaimsPayloadInterface) { + $actualClaims = []; + foreach ($payload->getClaims() as $claim) { + $actualClaims[$claim->getName()] = $claim->getValue(); + } + $this->assertEquals(json_decode($content, true), $actualClaims); + } + + $this->validateHeader($headers, $jws->getHeader()); + if ($unprotected === null) { + $this->assertEmpty($jws->getUnprotectedHeaders()); + } else { + $this->assertNotEmpty($jws->getUnprotectedHeaders()); + $this->validateHeader($unprotected, array_values($jws->getUnprotectedHeaders())[0]); + } + } + + private function validateHeader(array $expectedHeaders, HeaderInterface $actual): void + { + foreach ($expectedHeaders as $header => $value) { + $parameter = $actual->getParameter($header); + $this->assertNotNull($parameter); + $this->assertEquals($value, $parameter->getValue()); + } + } +} diff --git a/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/UnsecuredJwtFactoryTest.php b/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/UnsecuredJwtFactoryTest.php new file mode 100644 index 0000000000000..551f2c0b8c3fe --- /dev/null +++ b/app/code/Magento/JwtFrameworkAdapter/Test/Unit/Model/UnsecuredJwtFactoryTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\JwtFrameworkAdapter\Test\Unit\Model; + +use Magento\Framework\Jwt\HeaderInterface; +use Magento\Framework\Jwt\Payload\ArbitraryPayload; +use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; +use Magento\Framework\Jwt\Payload\NestedPayloadInterface; +use Magento\JwtFrameworkAdapter\Model\UnsecuredJwtFactory; +use PHPUnit\Framework\TestCase; + +class UnsecuredJwtFactoryTest extends TestCase +{ + /** + * @var UnsecuredJwtFactory + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->model = new UnsecuredJwtFactory(); + } + + public function getCreateCases(): array + { + return [ + 'compact-arbitrary' => [ + [['cty' => 'MyType', 'typ' => 'JWT']], + 'some-value', + null, + ArbitraryPayload::class + ], + 'compact-claims' => [ + [['typ' => 'JWT']], + '{"tst1":"val1","tst2":2,"tst3":true}', + null, + ClaimsPayloadInterface::class + ], + 'compact-nested' => [ + [['typ' => 'JWT', 'cty' => NestedPayloadInterface::CONTENT_TYPE]], + 'eyJhbGciOiJub25lIn0.' + .'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.', + null, + NestedPayloadInterface::class + ], + 'json-flat-arbitrary' => [ + [['typ' => 'JWT']], + 'arbitrary', + [['cty' => 'SomeType']], + ArbitraryPayload::class + ], + 'json-flat-claims' => [ + [['typ' => 'JWT']], + '{"tst1":"val1","tst2":2,"tst3":true}', + [['aud' => 'magento']], + ClaimsPayloadInterface::class + ], + 'json-arbitrary' => [ + [['typ' => 'JWT'], ['typ' => 'JWT', 'aud' => 'magento']], + 'value', + [['cty' => 'MyType'], ['cty' => 'MyType', 'crit' => 'exp']], + ArbitraryPayload::class + ] + ]; + } + + /** + * Test "create" method. + * + * @param array $headers + * @param string $content + * @param array|null $unprotected + * @param string $payloadClass + * @return void + * @dataProvider getCreateCases + */ + public function testCreate( + array $headers, + string $content, + ?array $unprotected, + string $payloadClass + ): void { + $jwt = $this->model->create($headers, $unprotected, $content); + + $payload = $jwt->getPayload(); + $this->assertEquals($content, $payload->getContent()); + $this->assertInstanceOf($payloadClass, $payload); + if ($payload instanceof ClaimsPayloadInterface) { + $actualClaims = []; + foreach ($payload->getClaims() as $claim) { + $actualClaims[$claim->getName()] = $claim->getValue(); + } + $this->assertEquals(json_decode($content, true), $actualClaims); + } + + $actualHeaders = array_map([$this, 'extractHeader'], $jwt->getProtectedHeaders()); + $this->assertEquals($headers, $actualHeaders); + } + + private function extractHeader(HeaderInterface $header): array + { + $values = []; + foreach ($header->getParameters() as $parameter) { + $values[$parameter->getName()] = $parameter->getValue(); + } + + return $values; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509ChainTest.php b/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509ChainTest.php new file mode 100644 index 0000000000000..93a2d469780d4 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509ChainTest.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Test\Unit\Header; + +use Magento\Framework\Jwt\Header\X509Chain; +use PHPUnit\Framework\TestCase; + +class X509ChainTest extends TestCase +{ + public function testEmpty(): void + { + $this->expectException(\InvalidArgumentException::class); + + new X509Chain([]); + } + + public function testGetValue(): void + { + $model = new X509Chain(['cert1', 'cert2']); + + $this->assertEquals('["Y2VydDE=","Y2VydDI="]', $model->getValue()); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha1ThumbprintTest.php b/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha1ThumbprintTest.php new file mode 100644 index 0000000000000..4d090a3c54bc6 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha1ThumbprintTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Test\Unit\Header; + +use Magento\Framework\Jwt\Header\X509Sha1Thumbprint; +use PHPUnit\Framework\TestCase; + +class X509Sha1ThumbprintTest extends TestCase +{ + public function testGetValue(): void + { + $model = new X509Sha1Thumbprint('cert:==somecert'); + + $this->assertEquals('Y2VydDo9PXNvbWVjZXJ0', $model->getValue()); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha256ThumbprintTest.php b/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha256ThumbprintTest.php new file mode 100644 index 0000000000000..6ca6075f5cedb --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Test/Unit/Header/X509Sha256ThumbprintTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Test\Unit\Header; + +use Magento\Framework\Jwt\Header\X509Sha256Thumbprint; +use PHPUnit\Framework\TestCase; + +class X509Sha256ThumbprintTest extends TestCase +{ + public function testGetValue(): void + { + $model = new X509Sha256Thumbprint('cert:=cert'); + + $this->assertEquals('Y2VydDo9Y2VydA', $model->getValue()); + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Test/Unit/Jwe/JweEncryptionJwksTest.php b/lib/internal/Magento/Framework/Jwt/Test/Unit/Jwe/JweEncryptionJwksTest.php new file mode 100644 index 0000000000000..aaf293fd28759 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Test/Unit/Jwe/JweEncryptionJwksTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Test\Unit\Jwe; + +use Magento\Framework\Jwt\Exception\EncryptionException; +use Magento\Framework\Jwt\Jwe\JweEncryptionJwks; +use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface; +use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\JwkSet; +use Magento\Framework\Jwt\Jws\JwsSignatureJwks; +use PHPUnit\Framework\TestCase; + +class JweEncryptionJwksTest extends TestCase +{ + public function getConstructorCases(): array + { + return [ + 'valid-jwk' => [$this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION), true], + 'valid-jwks' => [ + $this->createJwkSet( + [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION), + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION) + ] + ), + true + ], + 'invalid-jwk' => [$this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE), false], + 'invalid-jwks' => [ + $this->createJwkSet( + [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE), + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION) + ] + ), + false + ] + ]; + } + + /** + * Test constructor validation. + * + * @param Jwk|JwkSet $jwks + * @param bool $valid + * @return void + * @dataProvider getConstructorCases + */ + public function testConstruct($jwks, $valid): void + { + if (!$valid) { + $this->expectException(EncryptionException::class); + } + + new JweEncryptionJwks($jwks, JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM); + } + + public function getAlgorithmCases(): array + { + return [ + 'one-algo' => [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION, Jwk::ALGORITHM_RSA_OAEP), + Jwk::ALGORITHM_RSA_OAEP + ], + 'json' => [ + $this->createJwkSet( + [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION), + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION) + ] + ), + 'jwe-json-serialization' + ], + ]; + } + + /** + * Test algorithm logic. + * + * @param Jwk|JwkSet $jwk + * @param string $expectedName + * @return void + * @dataProvider getAlgorithmCases + */ + public function testGetAlgorithmName($jwk, string $expectedName): void + { + $model = new JweEncryptionJwks($jwk, JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM); + + $this->assertEquals($expectedName, $model->getAlgorithmName()); + } + + private function createJwk(string $use, string $alg = Jwk::ALGORITHM_RSA_OAEP_256): Jwk + { + $mock = $this->createMock(Jwk::class); + $mock->method('getPublicKeyUse')->willReturn($use); + $mock->method('getAlgorithm')->willReturn($alg); + + return $mock; + } + + public function createJwkSet(array $jwks): JwkSet + { + $mock = $this->createMock(JwkSet::class); + $mock->method('getKeys')->willReturn($jwks); + + return $mock; + } +} diff --git a/lib/internal/Magento/Framework/Jwt/Test/Unit/Jws/JwsSignatureJwksTest.php b/lib/internal/Magento/Framework/Jwt/Test/Unit/Jws/JwsSignatureJwksTest.php new file mode 100644 index 0000000000000..8ac53f5d5dd54 --- /dev/null +++ b/lib/internal/Magento/Framework/Jwt/Test/Unit/Jws/JwsSignatureJwksTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Jwt\Test\Unit\Jws; + +use Magento\Framework\Jwt\Exception\EncryptionException; +use Magento\Framework\Jwt\Jwk; +use Magento\Framework\Jwt\JwkSet; +use Magento\Framework\Jwt\Jws\JwsSignatureJwks; +use PHPUnit\Framework\TestCase; + +class JwsSignatureJwksTest extends TestCase +{ + public function getConstructorCases(): array + { + return [ + 'valid-jwk' => [$this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE), true], + 'valid-jwks' => [ + $this->createJwkSet( + [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE), + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE) + ] + ), + true + ], + 'invalid-jwk' => [$this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION), false], + 'invalid-jwks' => [ + $this->createJwkSet( + [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE), + $this->createJwk(Jwk::PUBLIC_KEY_USE_ENCRYPTION) + ] + ), + false + ] + ]; + } + + /** + * Test constructor validation. + * + * @param Jwk|JwkSet $jwks + * @param bool $valid + * @return void + * @dataProvider getConstructorCases + */ + public function testConstruct($jwks, $valid): void + { + if (!$valid) { + $this->expectException(EncryptionException::class); + } + + new JwsSignatureJwks($jwks); + } + + public function getAlgorithmCases(): array + { + return [ + 'one-algo' => [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE, Jwk::ALGORITHM_HS384), + Jwk::ALGORITHM_HS384 + ], + 'json' => [ + $this->createJwkSet( + [ + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE), + $this->createJwk(Jwk::PUBLIC_KEY_USE_SIGNATURE) + ] + ), + 'jws-json-serialization' + ], + ]; + } + + /** + * Test algorithm logic. + * + * @param Jwk|JwkSet $jwk + * @param string $expectedName + * @return void + * @dataProvider getAlgorithmCases + */ + public function testGetAlgorithmName($jwk, string $expectedName): void + { + $model = new JwsSignatureJwks($jwk); + + $this->assertEquals($expectedName, $model->getAlgorithmName()); + } + + private function createJwk(string $use, string $alg = Jwk::ALGORITHM_HS256): Jwk + { + $mock = $this->createMock(Jwk::class); + $mock->method('getPublicKeyUse')->willReturn($use); + $mock->method('getAlgorithm')->willReturn($alg); + + return $mock; + } + + public function createJwkSet(array $jwks): JwkSet + { + $mock = $this->createMock(JwkSet::class); + $mock->method('getKeys')->willReturn($jwks); + + return $mock; + } +} From ca9eb606b479f6680a03c364d8de1f89bfac1a94 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 23 Feb 2021 21:57:45 -0600 Subject: [PATCH 062/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Store/Model/Validation/StoreCodeValidator.php | 4 +++- .../Store/Model/Validation/StoreNameValidator.php | 9 +++++++-- .../Magento/Store/Model/Validation/StoreValidator.php | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php b/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php index 7db120ee2a47f..4dba36dfe935d 100644 --- a/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php +++ b/app/code/Magento/Store/Model/Validation/StoreCodeValidator.php @@ -41,7 +41,9 @@ public function isValid($value) ), \Zend_Validate_Regex::NOT_MATCH ); + $result = $validator->isValid($value); + $this->_messages = $validator->getMessages(); - return $validator->isValid($value); + return $result; } } diff --git a/app/code/Magento/Store/Model/Validation/StoreNameValidator.php b/app/code/Magento/Store/Model/Validation/StoreNameValidator.php index b03b25d6d6d3d..7d97a37cc9f55 100644 --- a/app/code/Magento/Store/Model/Validation/StoreNameValidator.php +++ b/app/code/Magento/Store/Model/Validation/StoreNameValidator.php @@ -34,8 +34,13 @@ public function __construct(NotEmptyFactory $notEmptyValidatorFactory) public function isValid($value) { $validator = $this->notEmptyValidatorFactory->create(['options' => []]); - $validator->setMessage(__('Name is required'), \Zend_Validate_NotEmpty::IS_EMPTY); + $validator->setMessage( + __('Name is required'), + \Zend_Validate_NotEmpty::IS_EMPTY + ); + $result = $validator->isValid($value); + $this->_messages = $validator->getMessages(); - return $validator->isValid($value); + return $result; } } diff --git a/app/code/Magento/Store/Model/Validation/StoreValidator.php b/app/code/Magento/Store/Model/Validation/StoreValidator.php index edebf043ef091..c98b5a4bea890 100644 --- a/app/code/Magento/Store/Model/Validation/StoreValidator.php +++ b/app/code/Magento/Store/Model/Validation/StoreValidator.php @@ -44,7 +44,9 @@ public function isValid($value) foreach ($this->rules as $fieldName => $rule) { $validator->addRule($rule, $fieldName); } + $result = $validator->isValid($value); + $this->_messages = $validator->getMessages(); - return $validator->isValid($value); + return $result; } } From 6327c2c292ca92819df08620a4900591278ad30c Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 24 Feb 2021 11:37:18 +0200 Subject: [PATCH 063/137] MC-40865: Create automated test for: ""Date and Time" attribute correctly rendered regarding timezone settings" --- .../Attribute/Frontend/DatetimeTest.php | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php index a18119b4e911a..77460f4b5ebce 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php @@ -12,12 +12,14 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** * Checks Datetime attribute's frontend model * + * @magentoAppArea frontend + * * @see \Magento\Eav\Model\Entity\Attribute\Frontend\Datetime */ class DatetimeTest extends TestCase @@ -43,25 +45,28 @@ class DatetimeTest extends TestCase private $productRepository; /** - * @var StoreManagerInterface + * @var DateTime */ - private $storeManager; + private $dateTime; /** - * @var DateTime + * @var ExecuteInStoreContext */ - private $dateTime; + private $executeInStoreContext; + /** + * @inheritdoc + */ protected function setUp(): void { parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); - $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->attributeRepository = $this->objectManager->get(ProductAttributeRepositoryInterface::class); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->productRepository->cleanCache(); $this->dateTime = $this->objectManager->create(DateTime::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); } /** @@ -80,14 +85,16 @@ public function testFrontendValueOnDifferentWebsites(): void $attribute = $this->attributeRepository->get('datetime_attribute'); $product = $this->productRepository->get('simple-on-two-websites'); $product->setDatetimeAttribute($this->dateTime->date('Y-m-d H:i:s')); - $valueOnWebsiteOne = $attribute->getFrontend()->getValue($product); - $secondStoreId = $this->storeManager->getStore('fixture_second_store')->getId(); - $this->storeManager->setCurrentStore($secondStoreId); - $valueOnWebsiteTwo = $attribute->getFrontend()->getValue($product); + $firstWebsiteValue = $attribute->getFrontend()->getValue($product); + $secondWebsiteValue = $this->executeInStoreContext->execute( + 'fixture_second_store', + [$attribute->getFrontend(), 'getValue'], + $product + ); $this->assertEquals( self::ONE_HOUR_IN_MILLISECONDS, - $this->dateTime->gmtTimestamp($valueOnWebsiteOne) - $this->dateTime->gmtTimestamp($valueOnWebsiteTwo), - 'The difference between the two time zones are incorrect' + $this->dateTime->gmtTimestamp($firstWebsiteValue) - $this->dateTime->gmtTimestamp($secondWebsiteValue), + 'The difference between values per different timezones is incorrect' ); } } From 6ad571e4c7f004c7ccf28c1f2536708e7542f623 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 24 Feb 2021 15:17:06 +0200 Subject: [PATCH 064/137] MC-40865: Create automated test for: ""Date and Time" attribute correctly rendered regarding timezone settings" --- .../Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php index 77460f4b5ebce..04e465c350db2 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DatetimeTest.php @@ -85,7 +85,11 @@ public function testFrontendValueOnDifferentWebsites(): void $attribute = $this->attributeRepository->get('datetime_attribute'); $product = $this->productRepository->get('simple-on-two-websites'); $product->setDatetimeAttribute($this->dateTime->date('Y-m-d H:i:s')); - $firstWebsiteValue = $attribute->getFrontend()->getValue($product); + $firstWebsiteValue = $this->executeInStoreContext->execute( + 'default', + [$attribute->getFrontend(), 'getValue'], + $product + ); $secondWebsiteValue = $this->executeInStoreContext->execute( 'fixture_second_store', [$attribute->getFrontend(), 'getValue'], From b2c47af0897c8253149462f2f1f9e1937be4c6ac Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 24 Feb 2021 15:35:45 +0200 Subject: [PATCH 065/137] magento/magento2#17727: `catalog_product_entity_media_gallery` not cleared when related products deleted. --- ...eImagesFromGalleryAfterRemovingProduct.php | 66 +++++++++++++ app/code/Magento/Catalog/etc/di.xml | 4 + .../ResourceModel/Product/GalleryTest.php | 92 +++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php diff --git a/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php b/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php new file mode 100644 index 0000000000000..ef3abddf91e69 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Gallery\ReadHandler; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; + +/** + * Responsible for deleting images from media gallery after deleting product + */ +class RemoveImagesFromGalleryAfterRemovingProduct +{ + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var ReadHandler + */ + private $mediaGalleryReadHandler; + + /** + * @param Gallery $galleryResource + * @param ReadHandler $mediaGalleryReadHandler + */ + public function __construct(Gallery $galleryResource, ReadHandler $mediaGalleryReadHandler) + { + $this->galleryResource = $galleryResource; + $this->mediaGalleryReadHandler = $mediaGalleryReadHandler; + } + + /** + * Delete media gallery after deleting product + * + * @param ProductRepositoryInterface $subject + * @param callable $proceed + * @param ProductInterface $product + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundDelete( + ProductRepositoryInterface $subject, + callable $proceed, + ProductInterface $product + ): bool { + $mediaGalleryAttributeId = $this->mediaGalleryReadHandler->getAttribute()->getAttributeId(); + $mediaGallery = $this->galleryResource->loadProductGalleryByAttributeId($product, $mediaGalleryAttributeId); + + $result = $proceed($product); + + if ($mediaGallery) { + $this->galleryResource->deleteGallery(array_column($mediaGallery, 'value_id')); + } + + return $result; + } +} diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 8a116282e2578..6840f557cbcb2 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -1319,4 +1319,8 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Api\ProductRepositoryInterface"> + <plugin name="remove_images_from_gallery_after_removing_product" + type="Magento\Catalog\Plugin\RemoveImagesFromGalleryAfterRemovingProduct"/> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php new file mode 100644 index 0000000000000..0fba4fa52112b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Gallery\ReadHandler; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\ResourceConnection; + +/** + * Test for \Magento\Catalog\Model\ResourceModel\Product\Gallery. + * + * @magentoAppArea adminhtml + */ +class GalleryTest extends TestCase +{ + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var ReadHandler + */ + private $readHandler; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + + $this->productRepository = $objectManager->create(ProductRepositoryInterface::class); + $this->galleryResource = $objectManager->create(Gallery::class); + $this->resource = $objectManager->create(ResourceConnection::class); + $this->readHandler = $objectManager->create(ReadHandler::class); + } + + /** + * Verify catalog_product_entity_media_gallery table will not have data after deleting the product + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testProductUpdate(): void + { + $product = $this->productRepository->get('simple'); + + $attributeId = $this->readHandler->getAttribute()->getAttributeId(); + $mediaGalleryData = $this->galleryResource->loadProductGalleryByAttributeId($product, $attributeId); + $values = array_column($mediaGalleryData, 'value_id'); + + $this->productRepository->delete($product); + + $this->assertEmpty($this->getMediaGalleryDataByValues($values)); + } + + /** + * Return data from catalog_product_entity_media_gallery_values table + * + * @param array $values + * @return array + */ + private function getMediaGalleryDataByValues(array $values): array + { + $connection = $this->resource->getConnection(); + $select = $connection->select() + ->from($this->resource->getTableName(Gallery::GALLERY_TABLE)) + ->where('value_id IN (?)', $values); + + return $connection->fetchAll($select); + } +} From b40d857ba375a657406cd66e2920f1831c36b4f1 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 24 Feb 2021 16:46:48 +0200 Subject: [PATCH 066/137] added assert to ensure that gallery data was existing --- .../Catalog/Model/ResourceModel/Product/GalleryTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php index 0fba4fa52112b..e7d78a73d79c5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/GalleryTest.php @@ -61,13 +61,14 @@ protected function setUp(): void * * @return void */ - public function testProductUpdate(): void + public function testDeleteProductWithImage(): void { $product = $this->productRepository->get('simple'); $attributeId = $this->readHandler->getAttribute()->getAttributeId(); $mediaGalleryData = $this->galleryResource->loadProductGalleryByAttributeId($product, $attributeId); $values = array_column($mediaGalleryData, 'value_id'); + $this->assertNotEmpty($this->getMediaGalleryDataByValues($values)); $this->productRepository->delete($product); From 1bdd266ef4b3a4b46a93107dc6503b25f2e5b495 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Thu, 25 Feb 2021 11:39:58 +0200 Subject: [PATCH 067/137] MC-40873: Product is shown as Out of Stock on CMS page when Category Permissions are enabled --- .../templates/product/widget/new/content/new_grid.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml index 5108c488aec19..66683ef328e08 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml @@ -89,7 +89,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> </button> <?php endif; ?> <?php else :?> - <?php if ($_item->getIsSalable()) :?> + <?php if ($_item->isAvailable()) :?> <div class="stock available"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> From 293f7a006f31c08a57c70c4e26c7a76fa8036c9d Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 25 Feb 2021 20:50:30 +0200 Subject: [PATCH 068/137] MQE-2540: Create automated test for: "Check that "AND" query is performed when searching using ElasticSearch 7" --- ...archUsingElasticSearchByProductSkuTest.xml | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml diff --git a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml new file mode 100644 index 0000000000000..92ef26f04bf6a --- /dev/null +++ b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontQuickSearchUsingElasticSearchByProductSkuTest"> + <annotations> + <features value="Elasticsearch7"/> + <stories value="Storefront Search"/> + <title value="Check that AND query is performed when searching using ElasticSearch 7"/> + <description value="Check that AND query is performed when searching using ElasticSearch 7"/> + <severity value="CRITICAL"/> + <testCaseId value="MQE-2540"/> + <useCaseId value="MC-29788"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="defaultSimpleProduct" stepKey="createFirtsSimpleProduct"> + <field key="sku">ABCDE</field> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createSecondSimpleProduct"> + <field key="sku">24 MB06</field> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createThirdSimpleProduct"> + <field key="sku">24 MB04</field> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createFourthSimpleProduct"> + <field key="sku">24 MB02</field> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createFifthSimpleProduct"> + <field key="sku">24 MB01</field> + </createData> + + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createFirtsSimpleProduct" stepKey="deleteProductOne"/> + <deleteData url="/V1/products/24+MB06" stepKey="deleteProductTwo"/> + <deleteData url="/V1/products/24+MB04" stepKey="deleteProductThree"/> + <deleteData url="/V1/products/24+MB02" stepKey="deleteProductFour"/> + <deleteData url="/V1/products/24+MB01" stepKey="deleteProductFive"/> + </after> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductSku"> + <argument name="phrase" value="$createThirdSimpleProduct.sku$"/> + </actionGroup> + <see userInput="4" selector="{{StorefrontCatalogSearchMainSection.productCount}}" stepKey="assertSearchResultCount"/> + <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertSecondProductName"> + <argument name="productName" value="$createSecondSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertThirdProductName"> + <argument name="productName" value="$createThirdSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertFourthProductName"> + <argument name="productName" value="$createFourthSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertFifthProductName"> + <argument name="productName" value="$createFifthSimpleProduct.name$"/> + </actionGroup> + </test> +</tests> From d56f33f1b309488f27db4e1474419d79b7ec36b4 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Fri, 26 Feb 2021 00:40:49 -0600 Subject: [PATCH 069/137] MC-40788: Customer is unable to convert all consumers to ampq - fixed --- .../MediaContentSynchronization/etc/queue.xml | 12 ++++++++++++ .../Magento/MediaGalleryRenditions/etc/queue.xml | 12 ++++++++++++ .../MediaGallerySynchronization/etc/queue.xml | 12 ++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/queue.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/queue.xml diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue.xml b/app/code/Magento/MediaContentSynchronization/etc/queue.xml new file mode 100644 index 0000000000000..9d1b994c602a4 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="media.content.synchronization" exchange="magento-db" type="db"> + <queue name="media.content.synchronization" consumer="media.content.synchronization" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\MediaContentSynchronization\Model\Consume::execute" /> + </broker> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue.xml new file mode 100644 index 0000000000000..fc86fe7ce4e53 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="media.gallery.renditions.update" exchange="magento-db" type="db"> + <queue name="media.gallery.renditions.update" consumer="media.gallery.renditions.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions::execute" /> + </broker> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue.xml new file mode 100644 index 0000000000000..a331673c7c1b7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="media.gallery.synchronization" exchange="magento-db" type="db"> + <queue name="media.gallery.synchronization" consumer="media.gallery.synchronization" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\MediaGallerySynchronization\Model\Consume::execute" /> + </broker> +</config> From f5f93d063c8324ab81ef78b6c034d8e5da301d35 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Fri, 26 Feb 2021 14:21:39 +0200 Subject: [PATCH 070/137] MC-40679: A "Products" page of the Admin panel is constantly loading after sorting by FPT value was applied --- .../Weee/Ui/Component/Listing/Columns.php | 49 +++++++++++++++++++ app/code/Magento/Weee/etc/di.xml | 3 ++ .../Weee/_files/fixed_product_attribute.php | 4 ++ 3 files changed, 56 insertions(+) create mode 100644 app/code/Magento/Weee/Ui/Component/Listing/Columns.php diff --git a/app/code/Magento/Weee/Ui/Component/Listing/Columns.php b/app/code/Magento/Weee/Ui/Component/Listing/Columns.php new file mode 100644 index 0000000000000..aaed4f46ee71c --- /dev/null +++ b/app/code/Magento/Weee/Ui/Component/Listing/Columns.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Ui\Component\Listing; + +use Magento\Catalog\Ui\Component\Listing\Attribute\Repository; +use Magento\Catalog\Ui\Component\Listing\Columns as DefaultColumns; +use Magento\Weee\Model\Attribute\Backend\Weee\Tax; + +/** + * Class Columns + */ +class Columns +{ + /** + * @var Repository + */ + private $attributeRepository; + + /** + * @param Repository $attributeRepository + */ + public function __construct( + Repository $attributeRepository + ) { + $this->attributeRepository = $attributeRepository; + } + + /** + * Makes column for FPT attribute in grid not sortable + * + * @param DefaultColumns $subject + */ + public function afterPrepare(DefaultColumns $subject) : void + { + foreach ($this->attributeRepository->getList() as $attribute) { + if ($attribute->getBackendModel() === Tax::class) { + $column = $subject->getComponent($attribute->getAttributeCode()); + $columnConfig = $column->getData('config'); + $columnConfig['sortable'] = false; + $column->setData('config', $columnConfig); + } + } + } +} diff --git a/app/code/Magento/Weee/etc/di.xml b/app/code/Magento/Weee/etc/di.xml index ccc849f4d8493..4bdc762b4e43e 100644 --- a/app/code/Magento/Weee/etc/di.xml +++ b/app/code/Magento/Weee/etc/di.xml @@ -84,4 +84,7 @@ <type name="Magento\Catalog\Model\ResourceModel\Attribute\RemoveProductAttributeData"> <plugin name="removeWeeAttributesData" type="Magento\Weee\Plugin\Catalog\ResourceModel\Attribute\RemoveProductWeeData" /> </type> + <type name="Magento\Catalog\Ui\Component\Listing\Columns"> + <plugin name="changeWeeColumnConfig" type="Magento\Weee\Ui\Component\Listing\Columns"/> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute.php b/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute.php index a74305d7db424..f75a10a07979f 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute.php @@ -6,6 +6,9 @@ declare(strict_types=1); +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/Weee/_files/fixed_product_attribute_rollback.php'); + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ @@ -27,6 +30,7 @@ 'attribute_group_id' => $attributeGroupId, 'frontend_input' => 'weee', 'frontend_label' => 'fixed product tax', + 'is_used_in_grid' => '1', ]; /** @var \Magento\Catalog\Model\Entity\Attribute $attribute */ From 05ab242701eb59fd4ec4d933a67e911dde1267f4 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Fri, 26 Feb 2021 15:01:11 +0200 Subject: [PATCH 071/137] MC-23989: Mini cart missing when you edit inline welcome message for guest and use special characters --- .../Magento/Theme/view/frontend/templates/html/header.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml index 3075e903bc174..c59ca42f50602 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml @@ -19,7 +19,7 @@ $welcomeMessage = $block->getWelcome(); <!-- /ko --> <!-- ko ifnot: customer().fullname --> <span class="not-logged-in" - data-bind="html: "<?= $escaper->escapeHtml($welcomeMessage) ?>"></span> + data-bind="html: '<?= $escaper->escapeHtmlAttr($welcomeMessage) ?>'"></span> <?= $block->getBlockHtml('header.additional') ?> <!-- /ko --> </li> From f8039e70b98dbf434a096f23308fa558fb384db3 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 26 Feb 2021 15:28:49 +0200 Subject: [PATCH 072/137] MQE-2540: Create automated test for: "Check that "AND" query is performed when searching using ElasticSearch 7" --- .../Catalog/Test/Mftf/Data/ProductData.xml | 16 ++++++++ ...archUsingElasticSearchByProductSkuTest.xml | 41 +++++++++---------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 5375459122e69..acd6c9a3058cb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -1397,4 +1397,20 @@ <requiredEntity type="product_option">ProductOptionField</requiredEntity> <requiredEntity type="product_option">ProductOptionField2</requiredEntity> </entity> + <entity name="SimpleProductWithCustomSku24MB01" type="product" extends="SimpleProduct2"> + <data key="name" unique="suffix">ProductWithSku24MB01-</data> + <data key="sku" unique="suffix">24 MB01</data> + </entity> + <entity name="SimpleProductWithCustomSku24MB02" type="product" extends="SimpleProduct2"> + <data key="name" unique="suffix">ProductWithSku24MB02-</data> + <data key="sku" unique="suffix">24 MB02 </data> + </entity> + <entity name="SimpleProductWithCustomSku24MB04" type="product" extends="SimpleProduct2"> + <data key="name" unique="suffix">ProductWithSku24MB04-</data> + <data key="sku" unique="suffix">24 MB04 </data> + </entity> + <entity name="SimpleProductWithCustomSku24MB06" type="product" extends="SimpleProduct2"> + <data key="name" unique="suffix">ProductWithSku24MB06-</data> + <data key="sku" unique="suffix">24 MB06 </data> + </entity> </entities> diff --git a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml index 92ef26f04bf6a..4cdd5dd37df52 100644 --- a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml +++ b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml @@ -15,26 +15,19 @@ <title value="Check that AND query is performed when searching using ElasticSearch 7"/> <description value="Check that AND query is performed when searching using ElasticSearch 7"/> <severity value="CRITICAL"/> - <testCaseId value="MQE-2540"/> + <testCaseId value="MC-31114"/> <useCaseId value="MC-29788"/> <group value="SearchEngineElasticsearch"/> </annotations> <before> - <createData entity="defaultSimpleProduct" stepKey="createFirtsSimpleProduct"> - <field key="sku">ABCDE</field> - </createData> - <createData entity="defaultSimpleProduct" stepKey="createSecondSimpleProduct"> - <field key="sku">24 MB06</field> - </createData> - <createData entity="defaultSimpleProduct" stepKey="createThirdSimpleProduct"> - <field key="sku">24 MB04</field> - </createData> - <createData entity="defaultSimpleProduct" stepKey="createFourthSimpleProduct"> - <field key="sku">24 MB02</field> - </createData> - <createData entity="defaultSimpleProduct" stepKey="createFifthSimpleProduct"> - <field key="sku">24 MB01</field> - </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProducts"/> + + <createData entity="VirtualProduct" stepKey="createFirtsSimpleProduct"/> + <createData entity="SimpleProductWithCustomSku24MB06" stepKey="createSecondSimpleProduct"/> + <createData entity="SimpleProductWithCustomSku24MB04" stepKey="createThirdSimpleProduct"/> + <createData entity="SimpleProductWithCustomSku24MB02" stepKey="createFourthSimpleProduct"/> + <createData entity="SimpleProductWithCustomSku24MB01" stepKey="createFifthSimpleProduct"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> <argument name="tags" value=""/> @@ -45,16 +38,22 @@ </before> <after> <deleteData createDataKey="createFirtsSimpleProduct" stepKey="deleteProductOne"/> - <deleteData url="/V1/products/24+MB06" stepKey="deleteProductTwo"/> - <deleteData url="/V1/products/24+MB04" stepKey="deleteProductThree"/> - <deleteData url="/V1/products/24+MB02" stepKey="deleteProductFour"/> - <deleteData url="/V1/products/24+MB01" stepKey="deleteProductFive"/> + + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToAdminCatalogProductPage"/> + <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterBundleProductOptionsDownToName"> + <argument name="name" value="productWithSku24MB0"/> + </actionGroup> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteAllProducts"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductSku"> - <argument name="phrase" value="$createThirdSimpleProduct.sku$"/> + <argument name="phrase" value="24 MB04"/> </actionGroup> + <see userInput="4" selector="{{StorefrontCatalogSearchMainSection.productCount}}" stepKey="assertSearchResultCount"/> + <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertSecondProductName"> <argument name="productName" value="$createSecondSimpleProduct.name$"/> </actionGroup> From 7aa440274777d73e52bd9718e994c424346ed515 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 26 Feb 2021 09:44:38 -0600 Subject: [PATCH 073/137] PWA-1326: Implement the schema changes for Configurable Options Selection - remove circular dependency --- app/code/Magento/ConfigurableProductGraphQl/composer.json | 1 - app/code/Magento/ConfigurableProductGraphQl/etc/module.xml | 1 - app/code/Magento/SwatchesGraphQl/composer.json | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index 72ecdbc3a375f..a6e1d1c822435 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -11,7 +11,6 @@ "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", "magento/module-catalog-inventory": "*", - "magento/module-swatches-graph-ql": "*", "magento/framework": "*" }, "license": [ diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml index e6345ac188631..3aa1658c9388d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml @@ -14,7 +14,6 @@ <module name="Magento_CatalogGraphQl"/> <module name="Magento_CatalogInventory"/> <module name="Magento_QuoteGraphQl"/> - <module name="Magento_SwatchesGraphQl"/> </sequence> </module> </config> diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 1b98b4044a2ff..8b6651f89f3b7 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -7,7 +7,8 @@ "magento/framework": "*", "magento/module-swatches": "*", "magento/module-catalog": "*", - "magento/module-catalog-graph-ql": "*" + "magento/module-catalog-graph-ql": "*", + "magento/module-configurable-product-graph-ql": "*" }, "license": [ "OSL-3.0", From 07f4838afe0d83cd26fa3d7523b4ce6d390a5268 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 26 Feb 2021 18:15:46 +0200 Subject: [PATCH 074/137] MQE-2540: Create automated test for: "Check that "AND" query is performed when searching using ElasticSearch 7" --- ...torefrontQuickSearchUsingElasticSearchByProductSkuTest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml index 4cdd5dd37df52..9bcf9fceb6049 100644 --- a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml +++ b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml @@ -40,12 +40,12 @@ <deleteData createDataKey="createFirtsSimpleProduct" stepKey="deleteProductOne"/> <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToAdminCatalogProductPage"/> - <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterBundleProductOptionsDownToName"> + <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterProductsGrid"> <argument name="name" value="productWithSku24MB0"/> </actionGroup> <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteAllProducts"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> </after> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductSku"> From f7903ab4de011ed44572d38f30ad66943dc05dd4 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 26 Feb 2021 18:16:03 -0600 Subject: [PATCH 075/137] PWA-1326: Implement the schema changes for Configurable Options Selection - renaming metadata back to original name for comparison purposes --- .../{OptionsSelection.php => OptionsSelectionMetadata.php} | 3 ++- .../Magento/ConfigurableProductGraphQl/etc/schema.graphqls | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/{OptionsSelection.php => OptionsSelectionMetadata.php} (97%) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php similarity index 97% rename from app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php rename to app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php index 6d1e4cbf5bb00..60369a5645868 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ConfigurableProductGraphQl\Model\Resolver; @@ -19,7 +20,7 @@ /** * Resolver for options selection */ -class OptionsSelection implements ResolverInterface +class OptionsSelectionMetadata implements ResolverInterface { /** * @var Metadata diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 9949ed1f8c417..d9ba786c84354 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -7,7 +7,7 @@ type Mutation { type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") - configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelection") + configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata") } type ConfigurableVariant @doc(description: "An array containing all the simple product variants of a configurable product") { From 7c5d129a441fc06bb65274e85f25da108de7dc9c Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Mon, 1 Mar 2021 10:13:48 +0200 Subject: [PATCH 076/137] MC-40679: A "Products" page of the Admin panel is constantly loading after sorting by FPT value was applied --- .../Magento/Weee/Model/Plugin/ColumnsTest.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php diff --git a/dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php b/dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php new file mode 100644 index 0000000000000..b1593300dd43c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Model\Plugin; + +use Magento\Catalog\Ui\Component\Listing\Attribute\Repository; +use Magento\Catalog\Ui\Component\Listing\Columns; +use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoDbIsolation enabled + */ +class ColumnsTest extends TestCase +{ + /** + * @var Columns + */ + private $columns; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $attributeRepository = $objectManager->get(Repository::class); + $dataProvider = $objectManager->create( + ProductDataProvider::class, + [ + 'name' => "product_listing_data_source", + 'primaryFieldName' => "entity_id", + 'requestFieldName' => "id", + ] + ); + $context = $objectManager->create(ContextInterface::class); + $context->setDataProvider($dataProvider); + $this->columns = $objectManager->create( + Columns::class, + ['attributeRepository' => $attributeRepository, 'context' => $context] + ); + } + + /** + * Check if FPT attribute column in product grid won't be sortable + * + * @magentoDataFixture Magento/Weee/_files/fixed_product_attribute.php + */ + public function testGetProductWeeeAttributesConfig() + { + $this->columns->prepare(); + $column = $this->columns->getComponent('fixed_product_attribute'); + $columnConfig = $column->getData('config'); + $this->assertArrayHasKey('sortable', $columnConfig); + $this->assertFalse($columnConfig['sortable']); + } +} From adacf15197d43c8278427c91980302a3f6f6956b Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Mon, 1 Mar 2021 15:02:50 +0200 Subject: [PATCH 077/137] MC-40873: Product is shown as Out of Stock on CMS page when Category Permissions are enabled --- .../Catalog/view/frontend/templates/product/compare/list.phtml | 2 +- .../Catalog/view/frontend/templates/product/list/items.phtml | 2 +- .../Catalog/view/frontend/templates/product/listing.phtml | 2 +- .../templates/product/widget/new/column/new_default_list.phtml | 2 +- .../templates/product/widget/new/content/new_list.phtml | 2 +- .../view/frontend/templates/product/widget/content/grid.phtml | 2 +- .../view/frontend/templates/catalog/product/type.phtml | 2 +- .../view/frontend/templates/product/widget/viewed/item.phtml | 2 +- .../widget/compared/column/compared_default_list.phtml | 2 +- .../templates/widget/compared/content/compared_grid.phtml | 2 +- .../templates/widget/compared/content/compared_list.phtml | 2 +- .../templates/widget/viewed/column/viewed_default_list.phtml | 2 +- .../frontend/templates/widget/viewed/content/viewed_grid.phtml | 2 +- .../frontend/templates/widget/viewed/content/viewed_list.phtml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 0bea3ca03dee8..bd37ad17f23ae 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -78,7 +78,7 @@ </button> </form> <?php else :?> - <?php if ($item->getIsSalable()) :?> + <?php if ($item->isAvailable()) :?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else :?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index e426b940deab7..6fd619de7fd6c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -287,7 +287,7 @@ $_item = null; </form> <?php endif; ?> <?php else:?> - <?php if ($_item->getIsSalable()):?> + <?php if ($_item->isAvailable()):?> <div class="stock available"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml index 6cebd51284f48..49dd702a6e39c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml @@ -63,7 +63,7 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); . ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->escapeUrl($block->getAddToCartUrl($_product)) . '"} }\'>' . '<span>' . $block->escapeHtml(__('Add to Cart')) . '</span></button>'; } else { - $info['button'] = $_product->getIsSalable() ? '<div class="stock available"><span>' . $block->escapeHtml(__('In stock')) . '</span></div>' : + $info['button'] = $_product->isAvailable() ? '<div class="stock available"><span>' . $block->escapeHtml(__('In stock')) . '</span></div>' : '<div class="stock unavailable"><span>' . $block->escapeHtml(__('Out of stock')) . '</span></div>'; } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml index 53a0682311b1f..fce91564c96a2 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml @@ -52,7 +52,7 @@ </button> <?php endif; ?> <?php else :?> - <?php if ($_product->getIsSalable()) :?> + <?php if ($_product->isAvailable()) :?> <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml index 378cd49493a6e..ceb32e78c7e44 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml @@ -88,7 +88,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> </button> <?php endif; ?> <?php else :?> - <?php if ($_item->getIsSalable()) :?> + <?php if ($_item->isAvailable()) :?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else :?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index 9637815c90eef..000f3ffd36934 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -77,7 +77,7 @@ use Magento\Framework\App\Action\Action; </button> </form> <?php else: ?> - <?php if ($_item->getIsSalable()): ?> + <?php if ($_item->isAvailable()): ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else: ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml b/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml index ba6d9e0abec71..e5397e758d63f 100644 --- a/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml +++ b/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml @@ -12,7 +12,7 @@ ?> <?php $_product = $block->getProduct() ?> -<?php if ($_product->getIsSalable()) : ?> +<?php if ($_product->isAvailable()) : ?> <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml index 562c9a2b63a99..da11582a16133 100644 --- a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml @@ -60,7 +60,7 @@ $rating = 'short'; </button> <?php endif; ?> <?php else : ?> - <?php if ($item->getIsSalable()) : ?> + <?php if ($item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml index a54259280e381..a9d5718449cd5 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml @@ -78,7 +78,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { <?php endif; ?> </div> <?php else : ?> - <?php if ($_product->getIsSalable()) : ?> + <?php if ($_product->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml index ad6b33820c752..1222490065185 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml @@ -83,7 +83,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml index ba7a50eef6485..6f7b4f4f66f27 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml @@ -84,7 +84,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml index 16fc2b070b95c..3e5cd15bbc62b 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml @@ -81,7 +81,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr <?php endif; ?> </div> <?php else : ?> - <?php if ($_product->getIsSalable()) : ?> + <?php if ($_product->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml index 567c3ebc57f9b..c2f98e72909d6 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml @@ -86,7 +86,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml index 9a8bb9c3b734f..32cf0bc69d1e5 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml @@ -88,7 +88,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> From 7218b6f3842f94a0235900f44ca239f1e6a0ae1a Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 1 Mar 2021 18:14:36 +0200 Subject: [PATCH 078/137] MQE-2540: Create automated test for: "Check that "AND" query is performed when searching using ElasticSearch 7" --- ...efrontQuickSearchUsingElasticSearchByProductSkuTest.xml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml index 9bcf9fceb6049..802553f20f7a9 100644 --- a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml +++ b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml @@ -39,12 +39,7 @@ <after> <deleteData createDataKey="createFirtsSimpleProduct" stepKey="deleteProductOne"/> - <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToAdminCatalogProductPage"/> - <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterProductsGrid"> - <argument name="name" value="productWithSku24MB0"/> - </actionGroup> - <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteAllProducts"/> - <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> + <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProductsAfterTest"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> </after> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> From 07592df80161067eb3ee0e62503685bf11b4f640 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 1 Mar 2021 10:32:53 -0600 Subject: [PATCH 079/137] PWA-1326: Implement the schema changes for Configurable Options Selection - combining schema with variant attribute details --- .../Products/DataProvider/ProductSearch.php | 10 --- .../Options/ConfigurableOptionsMetadata.php | 86 +++++++++++++++++++ .../Model/Options/Metadata.php | 2 +- .../Resolver/OptionsSelectionMetadata.php | 30 +++++-- .../Model/Resolver/SelectionMediaGallery.php | 4 +- 5 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index bcf0aa15b9e63..13bd29e83d87f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -18,8 +18,6 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\GraphQl\Model\Query\ContextInterface; /** @@ -89,7 +87,6 @@ public function __construct( * @param array $attributes * @param ContextInterface|null $context * @return SearchResultsInterface - * @throws GraphQlNoSuchEntityException */ public function getList( SearchCriteriaInterface $searchCriteria, @@ -110,13 +107,6 @@ public function getList( )->apply(); $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); - - try { - $collection->addMediaGalleryData(); - } catch (LocalizedException $e) { - throw new GraphQlNoSuchEntityException(__('Cannot load media gallery')); - } - $collection->load(); $this->collectionPostProcessor->process($collection, $attributes); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php new file mode 100644 index 0000000000000..5e0c5535c564c --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Options; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\ConfigurableProduct\Helper\Data; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; +use Magento\ConfigurableProductGraphQl\Model\Formatter\Option; + +/** + * Retrieve metadata for configurable option selection. + */ +class Metadata +{ + /** + * @var Data + */ + private $configurableProductHelper; + + /** + * @var Option + */ + private $configurableOptionsFormatter; + + /** + * @param Data $configurableProductHelper + * @param Option $configurableOptionsFormatter + */ + public function __construct( + Data $configurableProductHelper, + Option $configurableOptionsFormatter + ) { + $this->configurableProductHelper = $configurableProductHelper; + $this->configurableOptionsFormatter = $configurableOptionsFormatter; + } + + /** + * Load available selections from configurable options and variant. + * + * @param ProductInterface $product + * @param array $options + * @param array $selectedOptions + * @return array + */ + public function getAvailableSelections(ProductInterface $product, array $options, array $selectedOptions): array + { + $attributes = $this->getAttributes($product); + $availableSelections = []; + + foreach ($options as $attributeId => $option) { + if ($attributeId === 'index' || isset($selectedOptions[$attributeId])) { + continue; + } + + $availableSelections[] = $this->configurableOptionsFormatter->format( + $attributes[$attributeId], + $options[$attributeId] ?? [] + ); + } + + return $availableSelections; + } + + /** + * Retrieve configurable attributes for the product + * + * @param ProductInterface $product + * @return Attribute[] + */ + private function getAttributes(ProductInterface $product): array + { + $allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product); + $attributes = []; + foreach ($allowedAttributes as $attribute) { + $attributes[$attribute->getAttributeId()] = $attribute; + } + + return $attributes; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php index 5e0c5535c564c..ce81e970bcd58 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php @@ -16,7 +16,7 @@ /** * Retrieve metadata for configurable option selection. */ -class Metadata +class ConfigurableOptionsMetadata { /** * @var Data diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php index 60369a5645868..556aab7e39f7d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php @@ -9,6 +9,7 @@ use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProductGraphQl\Model\Formatter\Variant as VariantFormatter; +use Magento\ConfigurableProductGraphQl\Model\Options\ConfigurableOptionsMetadata; use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant; use Magento\ConfigurableProductGraphQl\Model\Options\Metadata; use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; @@ -27,6 +28,11 @@ class OptionsSelectionMetadata implements ResolverInterface */ private $configurableSelectionMetadata; + /** + * @var ConfigurableOptionsMetadata + */ + private $configurableOptionsMetadata; + /** * @var SelectionUidFormatter */ @@ -49,6 +55,7 @@ class OptionsSelectionMetadata implements ResolverInterface /** * @param Metadata $configurableSelectionMetadata + * @param ConfigurableOptionsMetadata $configurableOptionsMetadata * @param SelectionUidFormatter $selectionUidFormatter * @param Variant $variant * @param VariantFormatter $variantFormatter @@ -56,12 +63,14 @@ class OptionsSelectionMetadata implements ResolverInterface */ public function __construct( Metadata $configurableSelectionMetadata, + ConfigurableOptionsMetadata $configurableOptionsMetadata, SelectionUidFormatter $selectionUidFormatter, Variant $variant, VariantFormatter $variantFormatter, Data $configurableProductHelper ) { $this->configurableSelectionMetadata = $configurableSelectionMetadata; + $this->configurableOptionsMetadata = $configurableOptionsMetadata; $this->selectionUidFormatter = $selectionUidFormatter; $this->variant = $variant; $this->variantFormatter = $variantFormatter; @@ -85,14 +94,23 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $variants = $this->variant->getSalableVariantsByParent($product); $options = $this->configurableProductHelper->getOptions($product, $variants); + $configurableOptions = $this->configurableOptionsMetadata->getAvailableSelections( + $product, + $options, + $selectedOptions + ); + + $optionsAvailableForSelection = $this->configurableSelectionMetadata->getAvailableSelections( + $product, + $args['configurableOptionValueUids'] ?? [] + ); + return [ - 'configurable_options' => $this->configurableSelectionMetadata->getAvailableSelections( - $product, - $options, - $selectedOptions - ), + 'configurable_options' => $configurableOptions, 'variant' => $this->variantFormatter->format($options, $selectedOptions, $variants), - 'model' => $product + 'model' => $product, + 'options_available_for_selection' => $optionsAvailableForSelection['options_available_for_selection'], + 'availableSelectionProducts' => $optionsAvailableForSelection['availableSelectionProducts'] ]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php index 4aa322e66df60..972e4a9fd629a 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php @@ -21,11 +21,11 @@ class SelectionMediaGallery implements ResolverInterface */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($value['product']) || !$value['product']) { + if (!isset($value['model']) || !$value['model']) { return null; } - $product = $value['product']; + $product = $value['model']; $availableSelectionProducts = $value['availableSelectionProducts']; $mediaGalleryEntries = []; $usedProducts = $product->getTypeInstance()->getUsedProducts($product, null); From c9a6164a024a3f4b91790b30a1fb7bc60508619e Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 1 Mar 2021 10:38:56 -0600 Subject: [PATCH 080/137] PWA-1326: Implement the schema changes for Configurable Options Selection - combining schema with variant attribute details --- .../Options/ConfigurableOptionsMetadata.php | 2 +- .../Model/Options/Metadata.php | 147 ++++++++++++++---- .../ConfigurableOptionsSelectionTest.php | 5 - 3 files changed, 118 insertions(+), 36 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php index 5e0c5535c564c..ce81e970bcd58 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php @@ -16,7 +16,7 @@ /** * Retrieve metadata for configurable option selection. */ -class Metadata +class ConfigurableOptionsMetadata { /** * @var Data diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php index ce81e970bcd58..9fa6e4f23fa56 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php @@ -3,20 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\ConfigurableProductGraphQl\Model\Options; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\ConfigurableProduct\Helper\Data; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; -use Magento\ConfigurableProductGraphQl\Model\Formatter\Option; +use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant; +use Magento\Framework\Exception\NoSuchEntityException; /** * Retrieve metadata for configurable option selection. */ -class ConfigurableOptionsMetadata +class Metadata { /** * @var Data @@ -24,63 +24,150 @@ class ConfigurableOptionsMetadata private $configurableProductHelper; /** - * @var Option + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + /** + * @var ProductRepositoryInterface */ - private $configurableOptionsFormatter; + private $productRepository; + + /** + * @var Variant + */ + private $variant; /** * @param Data $configurableProductHelper - * @param Option $configurableOptionsFormatter + * @param SelectionUidFormatter $selectionUidFormatter + * @param ProductRepositoryInterface $productRepository + * @param Variant $variant */ public function __construct( Data $configurableProductHelper, - Option $configurableOptionsFormatter + SelectionUidFormatter $selectionUidFormatter, + ProductRepositoryInterface $productRepository, + Variant $variant ) { $this->configurableProductHelper = $configurableProductHelper; - $this->configurableOptionsFormatter = $configurableOptionsFormatter; + $this->selectionUidFormatter = $selectionUidFormatter; + $this->productRepository = $productRepository; + $this->variant = $variant; } /** - * Load available selections from configurable options and variant. + * Load available selections from configurable options. * * @param ProductInterface $product - * @param array $options - * @param array $selectedOptions + * @param array $selectedOptionsUid * @return array + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function getAvailableSelections(ProductInterface $product, array $options, array $selectedOptions): array - { - $attributes = $this->getAttributes($product); - $availableSelections = []; + public function getAvailableSelections( + ProductInterface $product, + array $selectedOptionsUid + ): array { + $options = $this->configurableProductHelper->getOptions($product, $this->getAllowProducts($product)); + $selectedOptions = $this->selectionUidFormatter->extract($selectedOptionsUid); + $attributeCodes = $this->getAttributeCodes($product); + $availableSelections = $availableProducts = $variantData = []; - foreach ($options as $attributeId => $option) { - if ($attributeId === 'index' || isset($selectedOptions[$attributeId])) { - continue; + if (isset($options['index']) && $options['index']) { + foreach ($options['index'] as $productId => $productOptions) { + if (!empty($selectedOptions) && !$this->hasProductRequiredOptions($selectedOptions, $productOptions)) { + continue; + } + + $availableProducts[] = $productId; + foreach ($productOptions as $attributeId => $optionIndex) { + $uid = $this->selectionUidFormatter->encode($attributeId, (int)$optionIndex); + + if (isset($availableSelections[$attributeId]['option_value_uids']) + && in_array($uid, $availableSelections[$attributeId]['option_value_uids']) + ) { + continue; + } + $availableSelections[$attributeId]['option_value_uids'][] = $uid; + $availableSelections[$attributeId]['attribute_code'] = $attributeCodes[$attributeId]; + } + + if ($this->hasSelectionProduct($selectedOptions, $productOptions)) { + $variantProduct = $this->productRepository->getById($productId); + $variantData = $variantProduct->getData(); + $variantData['model'] = $variantProduct; + } } + } + + return [ + 'options_available_for_selection' => $availableSelections, + 'variant' => $variantData, + 'availableSelectionProducts' => array_unique($availableProducts), + 'product' => $product + ]; + } + + /** + * Get allowed products. + * + * @param ProductInterface $product + * @return ProductInterface[] + */ + public function getAllowProducts(ProductInterface $product): array + { + return $this->variant->getSalableVariantsByParent($product) ?? []; + } - $availableSelections[] = $this->configurableOptionsFormatter->format( - $attributes[$attributeId], - $options[$attributeId] ?? [] - ); + /** + * Check if a product has the selected options. + * + * @param array $requiredOptions + * @param array $productOptions + * @return bool + */ + private function hasProductRequiredOptions($requiredOptions, $productOptions): bool + { + $result = true; + foreach ($requiredOptions as $attributeId => $optionIndex) { + if (!isset($productOptions[$attributeId]) || !$productOptions[$attributeId] + || $optionIndex != $productOptions[$attributeId] + ) { + $result = false; + break; + } } - return $availableSelections; + return $result; + } + + /** + * Check if selected options match a product. + * + * @param array $requiredOptions + * @param array $productOptions + * @return bool + */ + private function hasSelectionProduct($requiredOptions, $productOptions): bool + { + return $this->hasProductRequiredOptions($productOptions, $requiredOptions); } /** - * Retrieve configurable attributes for the product + * Retrieve attribute codes * * @param ProductInterface $product - * @return Attribute[] + * @return string[] */ - private function getAttributes(ProductInterface $product): array + private function getAttributeCodes(ProductInterface $product): array { $allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product); - $attributes = []; + $attributeCodes = []; foreach ($allowedAttributes as $attribute) { - $attributes[$attribute->getAttributeId()] = $attribute; + $attributeCodes[$attribute->getAttributeId()] = $attribute->getProductAttribute()->getAttributeCode(); } - return $attributes; + return $attributeCodes; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php index 7ea02a0fa1a45..ac252acfcaa2b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php @@ -135,10 +135,6 @@ public function testSelectedVariant(): void self::assertIsString($product['configurable_product_options_selection']['variant']['sku']); $urlKey = 'configurable-option-first-option-1-second-option-1'; self::assertEquals($urlKey, $product['configurable_product_options_selection']['variant']['url_key']); - self::assertMatchesRegularExpression( - "/{$urlKey}/", - $product['configurable_product_options_selection']['variant']['url_path'] - ); $this->assertMediaGallery($product); } @@ -264,7 +260,6 @@ private function getQuery( uid sku url_key - url_path } media_gallery { url From 00773eb7510d231d273b84c5808d535dfc9fb382 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 2 Mar 2021 10:16:16 +0200 Subject: [PATCH 081/137] MC-40679: A "Products" page of the Admin panel is constantly loading after sorting by FPT value was applied --- .../Weee/{ => Plugin/Catalog}/Ui/Component/Listing/Columns.php | 2 +- app/code/Magento/Weee/etc/di.xml | 2 +- .../Catalog/Ui/Component/Listing}/ColumnsTest.php | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) rename app/code/Magento/Weee/{ => Plugin/Catalog}/Ui/Component/Listing/Columns.php (95%) rename dev/tests/integration/testsuite/Magento/Weee/{Model/Plugin => Plugin/Catalog/Ui/Component/Listing}/ColumnsTest.php (98%) diff --git a/app/code/Magento/Weee/Ui/Component/Listing/Columns.php b/app/code/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/Columns.php similarity index 95% rename from app/code/Magento/Weee/Ui/Component/Listing/Columns.php rename to app/code/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/Columns.php index aaed4f46ee71c..8716884b6a5ea 100644 --- a/app/code/Magento/Weee/Ui/Component/Listing/Columns.php +++ b/app/code/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/Columns.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Weee\Ui\Component\Listing; +namespace Magento\Weee\Plugin\Catalog\Ui\Component\Listing; use Magento\Catalog\Ui\Component\Listing\Attribute\Repository; use Magento\Catalog\Ui\Component\Listing\Columns as DefaultColumns; diff --git a/app/code/Magento/Weee/etc/di.xml b/app/code/Magento/Weee/etc/di.xml index 4bdc762b4e43e..fa3fafc5a914a 100644 --- a/app/code/Magento/Weee/etc/di.xml +++ b/app/code/Magento/Weee/etc/di.xml @@ -85,6 +85,6 @@ <plugin name="removeWeeAttributesData" type="Magento\Weee\Plugin\Catalog\ResourceModel\Attribute\RemoveProductWeeData" /> </type> <type name="Magento\Catalog\Ui\Component\Listing\Columns"> - <plugin name="changeWeeColumnConfig" type="Magento\Weee\Ui\Component\Listing\Columns"/> + <plugin name="changeWeeColumnConfig" type="Magento\Weee\Plugin\Catalog\Ui\Component\Listing\Columns"/> </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php b/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php similarity index 98% rename from dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php rename to dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php index b1593300dd43c..28ecf11668b8f 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/Model/Plugin/ColumnsTest.php +++ b/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php @@ -5,8 +5,6 @@ */ declare(strict_types=1); -namespace Magento\Weee\Model\Plugin; - use Magento\Catalog\Ui\Component\Listing\Attribute\Repository; use Magento\Catalog\Ui\Component\Listing\Columns; use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider; From f370a8e02166bec7c81fd115bef5020b54ac6df1 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Tue, 2 Mar 2021 15:25:18 +0200 Subject: [PATCH 082/137] MC-40122: Double spaces not displaying for SKU in product grid --- .../Catalog/Test/Mftf/Data/ProductData.xml | 4 ++ .../AdminShowDoubleSpacesInProductGrid.xml | 52 +++++++++++++++++++ .../ui_component/product_listing.xml | 4 +- .../web/template/grid/cells/preserved.html | 7 +++ .../web/css/source/module/_data-grid.less | 4 ++ 5 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 5375459122e69..f7622140c97cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -39,6 +39,10 @@ <data key="name">TestFooBar</data> <data key="sku" unique="suffix">foobar</data> </entity> + <entity name="ApiSimpleProductWithDoubleSpaces" type="product" extends="ApiSimpleProduct"> + <data key="name">Simple Product Double Space</data> + <data key="sku" unique="suffix">simple-product double-space</data> + </entity> <entity name="ApiSimpleProductWithSpecCharInName" type="product" extends="ApiSimpleProduct"> <data key="name">Pursuit Lumaflex&trade; Tone Band</data> <data key="sku" unique="suffix">x&trade;</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml new file mode 100644 index 0000000000000..c3e939b4155c8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminShowDoubleSpacesInProductGrid"> + <annotations> + <features value="Catalog"/> + <stories value="Edit products"/> + <title value="Show double spaces in the product grid"/> + <description value="Admin should be able to see double spaces in the Name and Sku fields in the product grid"/> + <testCaseId value="MC-40725"/> + <useCaseId value="MC-40122"/> + <severity value="AVERAGE"/> + <group value="Catalog"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithDoubleSpaces" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="cronRun"/> + <magentoCLI command="cron:run --group=index" stepKey="cronRunSecondTime"/> + </before> + + <after> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteProduct"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="searchForProduct"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="assertProductName"> + <argument name="column" value="Name"/> + <argument name="value" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="assertProductSku"> + <argument name="column" value="SKU"/> + <argument name="value" value="$createProduct.sku$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index 88bb578712056..2cd2a15b04900 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -132,7 +132,7 @@ <settings> <addField>true</addField> <filter>text</filter> - <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <bodyTmpl>Magento_Catalog/grid/cells/preserved</bodyTmpl> <label translate="true">Name</label> </settings> </column> @@ -155,7 +155,7 @@ <column name="sku" sortOrder="60"> <settings> <filter>text</filter> - <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <bodyTmpl>Magento_Catalog/grid/cells/preserved</bodyTmpl> <label translate="true">SKU</label> </settings> </column> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html b/app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html new file mode 100644 index 0000000000000..936342df23795 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html @@ -0,0 +1,7 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="data-grid-cell-content white-space-preserved" html="$col.getLabel($row())"/> diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less index 3e9f2d4401b05..d88260e01b25d 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less @@ -66,6 +66,10 @@ display: inline-block; overflow: hidden; width: 100%; + + &.white-space-preserved { + white-space: pre; + } } body._in-resize { From f58b284bff71dff68dce558496b0e962c2b7df00 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 2 Mar 2021 15:44:29 +0200 Subject: [PATCH 083/137] MC-40679: A "Products" page of the Admin panel is constantly loading after sorting by FPT value was applied --- .../Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php b/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php index 28ecf11668b8f..7889d214b7dd4 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php +++ b/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php @@ -5,6 +5,8 @@ */ declare(strict_types=1); +namespace Magento\Weee\Plugin\Catalog\Ui\Component\Listing; + use Magento\Catalog\Ui\Component\Listing\Attribute\Repository; use Magento\Catalog\Ui\Component\Listing\Columns; use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider; From f389d5083cf71c8679143e9e213ddc7069cc6045 Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Tue, 2 Mar 2021 08:05:16 -0600 Subject: [PATCH 084/137] MC-40817: Unable to open fullscreen video using mobile browsers --- lib/web/fotorama/fotorama.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 38c26bf8f94bd..01c89deaef95a 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -1903,6 +1903,11 @@ fotoramaVersion = '4.6.4'; }); } + // check if current media object is YouTube or Vimeo video stream + function isVideo() { + return $((that.activeFrame || {}).$stageFrame || {}).hasClass('fotorama-video-container'); + } + function allowKey(key) { return o_keyboard[key]; } @@ -3149,8 +3154,7 @@ fotoramaVersion = '4.6.4'; if (o_allowFullScreen && !that.fullScreen) { //check that this is not video - var isVideo = $((that.activeFrame || {}).$stageFrame || {}).hasClass('fotorama-video-container'); - if(isVideo) { + if(isVideo()) { return; } @@ -3740,7 +3744,9 @@ fotoramaVersion = '4.6.4'; activeIndexes = []; - // detachFrames(STAGE_FRAME_KEY); + if (!isVideo()) { + detachFrames(STAGE_FRAME_KEY); + } reset.ok = true; From 7dbbfcd9388b601434d345a2a2eb336ba013d9eb Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Tue, 2 Mar 2021 11:04:16 -0600 Subject: [PATCH 085/137] MC-40817: Unable to open fullscreen video using mobile browsers --- lib/web/fotorama/fotorama.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 01c89deaef95a..e0a991b6c5ee3 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -1903,7 +1903,10 @@ fotoramaVersion = '4.6.4'; }); } - // check if current media object is YouTube or Vimeo video stream + /** + * Checks if current media object is YouTube or Vimeo video stream + * @returns {boolean} + */ function isVideo() { return $((that.activeFrame || {}).$stageFrame || {}).hasClass('fotorama-video-container'); } From 7938fb9506178f608fe6d3f5f6c572d8804831fb Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Tue, 2 Mar 2021 11:46:19 -0600 Subject: [PATCH 086/137] B2B-1632: Add MFTF test for MC-38948 - Adding helpers to assert against expected files that match pattern - Adding S3 test for scheduled export --- .../Test/Mftf/Helper/S3FileAssertions.php | 33 +++++++++++++++++ .../Test/Mftf/Helper/LocalFileAssertions.php | 35 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php index e896901211cb1..96b2d77c951b8 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php +++ b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php @@ -149,6 +149,21 @@ public function assertFileExists($filePath, $message = ''): void $this->assertTrue($this->driver->isExists($filePath), $message); } + /** + * Asserts that a file with the given glob pattern exists in the given path on the remote storage system + * + * @param string $path + * @param string $pattern + * @param string $message + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertSearchedForFileExists($path, $pattern, $message = ""): void + { + $files = $this->driver->search($pattern, $path); + $this->assertNotEmpty($files, $message); + } + /** * Assert a file does not exist on the remote storage system * @@ -206,6 +221,24 @@ public function assertFileContainsString($filePath, $text, $message = ""): void $this->assertStringContainsString($text, $this->driver->fileGetContents($filePath), $message); } + /** + * Asserts that a file with the given glob pattern at the given path on the remote storage system contains a given string + * + * @param string $path + * @param string $pattern + * @param string $text + * @param int $fileIndex + * @param string $message + * @return void + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertSearchedForFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void + { + $files = $this->driver->search($pattern, $path); + $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex]), $message); + } + /** * Assert a file on the remote storage system does not contain a given string * diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php index ed0e244c280dc..cb12f6a731bb1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php +++ b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php @@ -134,6 +134,22 @@ public function assertFileExists($filePath, $message = ''): void $this->assertTrue($this->driver->isExists($realPath), $message); } + /** + * Asserts that a file with the given glob pattern exists in the given path + * + * @param string $path + * @param string $pattern + * @param string $message + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertSearchedForFileExists($path, $pattern, $message = ""): void + { + $realPath = $this->expandPath($path); + $files = $this->driver->search($pattern, $realPath); + $this->assertNotEmpty($files, $message); + } + /** * Assert a file does not exist * @@ -195,6 +211,25 @@ public function assertFileContainsString($filePath, $text, $message = ""): void $this->assertStringContainsString($text, $this->driver->fileGetContents($realPath), $message); } + /** + * Asserts that a file with the given glob pattern at the given path contains a given string + * + * @param string $path + * @param string $pattern + * @param string $text + * @param int $fileIndex + * @param string $message + * @return void + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertSearchedForFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void + { + $realPath = $this->expandPath($path); + $files = $this->driver->search($pattern, $realPath); + $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex]), $message); + } + /** * Assert a file does not contain a given string * From 40c6f044dee1453b4b0e29ddf6d1dfa88343bc9e Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 1 Mar 2021 12:40:40 -0600 Subject: [PATCH 087/137] MC-40943: Product import: tax_class_name: None creates a duplicate option - Fix product import with tax_class_name "none" creates a new tax tax class - Fix product import with tax_class_name "0" does not update product tax class --- .../Model/Import/Product.php | 2 +- .../Import/Product/TaxClassProcessor.php | 52 ++++++++++++++----- .../Import/Product/TaxClassProcessorTest.php | 18 +++++++ .../Model/Import/ProductTest.php | 38 ++++++++++++++ .../_files/product_tax_class_none_import.csv | 3 ++ 5 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_tax_class_none_import.csv diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 673dbcb3b3c99..d0c93658fc282 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1855,7 +1855,7 @@ protected function _saveProducts() } $productTypeModel = $this->_productTypeModels[$productType]; - if (!empty($rowData['tax_class_name'])) { + if (isset($rowData['tax_class_name']) && strlen($rowData['tax_class_name'])) { $rowData['tax_class_id'] = $this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel); } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php index af102cc50b8b9..e068a07404822 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php @@ -7,9 +7,25 @@ use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; use Magento\Tax\Model\ClassModel; +use Magento\Tax\Model\ClassModelFactory; +use Magento\Tax\Model\ResourceModel\TaxClass\Collection; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory; +/** + * Imported products tax class processor + */ class TaxClassProcessor { + /** + * Empty tax class name + */ + private const CLASS_NONE_NAME = 'none'; + + /** + * Empty tax class ID + */ + private const CLASS_NONE_ID = 0; + /** * Tax attribute code. */ @@ -25,24 +41,24 @@ class TaxClassProcessor /** * Instance of tax class collection factory. * - * @var \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory + * @var CollectionFactory */ protected $collectionFactory; /** * Instance of tax model factory. * - * @var \Magento\Tax\Model\ClassModelFactory + * @var ClassModelFactory */ protected $classModelFactory; /** - * @param \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory $collectionFactory - * @param \Magento\Tax\Model\ClassModelFactory $classModelFactory + * @param CollectionFactory $collectionFactory + * @param ClassModelFactory $classModelFactory */ public function __construct( - \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory $collectionFactory, - \Magento\Tax\Model\ClassModelFactory $classModelFactory + CollectionFactory $collectionFactory, + ClassModelFactory $classModelFactory ) { $this->collectionFactory = $collectionFactory; $this->classModelFactory = $classModelFactory; @@ -59,9 +75,9 @@ protected function initTaxClasses() if (empty($this->taxClasses)) { $collection = $this->collectionFactory->create(); $collection->addFieldToFilter('class_type', ClassModel::TAX_CLASS_TYPE_PRODUCT); - /* @var $collection \Magento\Tax\Model\ResourceModel\TaxClass\Collection */ + /* @var $collection Collection */ foreach ($collection as $taxClass) { - $this->taxClasses[$taxClass->getClassName()] = $taxClass->getId(); + $this->taxClasses[mb_strtolower($taxClass->getClassName())] = $taxClass->getId(); } } return $this; @@ -76,7 +92,7 @@ protected function initTaxClasses() */ protected function createTaxClass($taxClassName, AbstractType $productTypeModel) { - /** @var \Magento\Tax\Model\ClassModelFactory $taxClass */ + /** @var ClassModelFactory $taxClass */ $taxClass = $this->classModelFactory->create(); $taxClass->setClassType(ClassModel::TAX_CLASS_TYPE_PRODUCT); $taxClass->setClassName($taxClassName); @@ -98,10 +114,22 @@ protected function createTaxClass($taxClassName, AbstractType $productTypeModel) */ public function upsertTaxClass($taxClassName, AbstractType $productTypeModel) { - if (!isset($this->taxClasses[$taxClassName])) { - $this->taxClasses[$taxClassName] = $this->createTaxClass($taxClassName, $productTypeModel); + $normalizedTaxClassName = mb_strtolower($taxClassName); + + if ($normalizedTaxClassName === (string) self::CLASS_NONE_ID) { + $normalizedTaxClassName = self::CLASS_NONE_NAME; + } + + if (!isset($this->taxClasses[$normalizedTaxClassName])) { + $this->taxClasses[$normalizedTaxClassName] = $normalizedTaxClassName === self::CLASS_NONE_NAME + ? self::CLASS_NONE_ID + : $this->createTaxClass($taxClassName, $productTypeModel); + } + if ($normalizedTaxClassName === self::CLASS_NONE_NAME) { + // Add None option to tax_class_id options. + $productTypeModel->addAttributeOption(self::ATRR_CODE, self::CLASS_NONE_ID, self::CLASS_NONE_ID); } - return $this->taxClasses[$taxClassName]; + return $this->taxClasses[$normalizedTaxClassName]; } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php index ba65ffa37c8c7..80ee5e92f2e79 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php @@ -101,4 +101,22 @@ public function testUpsertTaxClassNotExist() $taxClassId = $this->taxClassProcessor->upsertTaxClass('noExistClassName', $this->product); $this->assertEquals(self::TEST_JUST_CREATED_TAX_CLASS_ID, $taxClassId); } + + public function testUpsertTaxClassExistCaseInsensitive() + { + $taxClassId = $this->taxClassProcessor->upsertTaxClass(strtoupper(self::TEST_TAX_CLASS_NAME), $this->product); + $this->assertEquals(self::TEST_TAX_CLASS_ID, $taxClassId); + } + + public function testUpsertTaxClassNone() + { + $taxClassId = $this->taxClassProcessor->upsertTaxClass('none', $this->product); + $this->assertEquals(0, $taxClassId); + } + + public function testUpsertTaxClassZero() + { + $taxClassId = $this->taxClassProcessor->upsertTaxClass(0, $this->product); + $this->assertEquals(0, $taxClassId); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index ceb07e3445c0e..fc4315e0baa15 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -3526,4 +3526,42 @@ public function testImportInvalidAdditionalImages(): void $errors->getErrorByRowNumber(0)[0]->getErrorMessage() ); } + + /** + * Test that product tax classes "none", "0" are imported correctly + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoAppIsolation enabled + */ + public function testImportProductWithTaxClassNone(): void + { + $pathToFile = __DIR__ . '/_files/product_tax_class_none_import.csv'; + $importModel = $this->createImportModel($pathToFile); + $this->assertErrorsCount(0, $importModel->validateData()); + $importModel->importData(); + $simpleProduct = $this->getProductBySku('simple'); + $this->assertSame('0', (string) $simpleProduct->getTaxClassId()); + $simpleProduct = $this->getProductBySku('simple2'); + $this->assertSame('0', (string) $simpleProduct->getTaxClassId()); + } + + /** + * @param int $count + * @param ProcessingErrorAggregatorInterface $errors + */ + private function assertErrorsCount(int $count, ProcessingErrorAggregatorInterface $errors): void + { + $this->assertEquals( + $count, + $errors->getErrorsCount(), + array_reduce( + $errors->getAllErrors(), + function ($output, $error) { + return "$output\n{$error->getErrorMessage()}"; + }, + '' + ) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_tax_class_none_import.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_tax_class_none_import.csv new file mode 100644 index 0000000000000..a53a9ef043ba2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_tax_class_none_import.csv @@ -0,0 +1,3 @@ +"sku","tax_class_name" +"simple","none" +"simple2","0" From 13b09134de0df6203ed99b5df7a8e5caa1af11e9 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 2 Mar 2021 14:30:52 -0600 Subject: [PATCH 088/137] PWA-1326: Implement the schema changes for Configurable Options Selection - refactoring swatches --- .../Model/Formatter/OptionValue.php | 11 +------- .../Model/Options/DataProvider/Variant.php | 5 ++-- .../SwatchesGraphQl/etc/schema.graphqls | 2 +- .../Swatches/ProductSwatchDataTest.php | 28 ++++++++++++++++--- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php index f171514a37897..5d721f13fbb9d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php @@ -11,7 +11,6 @@ use Magento\CatalogInventory\Model\StockRegistry; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; -use Magento\SwatchesGraphQl\Model\Resolver\Product\Options\DataProvider\SwatchDataProvider; /** * Formatter for configurable product option values @@ -23,11 +22,6 @@ class OptionValue */ private $selectionUidFormatter; - /** - * @var SwatchDataProvider - */ - private $swatchDataProvider; - /** * @var StockRegistry */ @@ -35,16 +29,13 @@ class OptionValue /** * @param SelectionUidFormatter $selectionUidFormatter - * @param SwatchDataProvider $swatchDataProvider * @param StockRegistry $stockRegistry */ public function __construct( SelectionUidFormatter $selectionUidFormatter, - SwatchDataProvider $swatchDataProvider, StockRegistry $stockRegistry ) { $this->selectionUidFormatter = $selectionUidFormatter; - $this->swatchDataProvider = $swatchDataProvider; $this->stockRegistry = $stockRegistry; } @@ -69,7 +60,7 @@ public function format(array $optionValue, Attribute $attribute, array $optionId 'is_available' => $this->getIsAvailable($optionIds[$valueIndex] ?? []), 'is_use_default' => (bool)$attribute->getIsUseDefault(), 'label' => $optionValue['label'], - 'swatch' => $this->swatchDataProvider->getData($optionValue['value_index']) + 'value_index' => $optionValue['value_index'] ]; } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php index 71671353facf5..7b5a3fb806a5f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php @@ -10,7 +10,6 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; /** @@ -44,8 +43,8 @@ public function __construct( * Load available child products by parent * * @param ProductInterface $product - * @return DataObject[] - * @throws LocalizedException + * @return ProductInterface[] + * @throws \Magento\Framework\Exception\LocalizedException */ public function getSalableVariantsByParent(ProductInterface $product): array { diff --git a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls index 35f6265093990..3491568108daf 100644 --- a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls @@ -49,5 +49,5 @@ type ColorSwatchData implements SwatchDataInterface { } type ConfigurableProductOptionValue { - swatch: SwatchDataInterface + swatch: SwatchDataInterface @resolver(class: "Magento\\SwatchesGraphQl\\Model\\Resolver\\Product\\Options\\SwatchData") } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php index ae34ea31f0d51..6c02882af8ecc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php @@ -84,12 +84,12 @@ public function testVisualSwatchDataValues() $color = '#000000'; $query = <<<QUERY { - products(filter: {sku: {eq: "$productSku"}}) { + products(filter: { sku: { eq: "$productSku" } }) { items { - ... on ConfigurableProduct{ - configurable_options{ + ... on ConfigurableProduct { + configurable_options { values { - swatch_data{ + swatch_data { value ... on ImageSwatchData { thumbnail @@ -97,6 +97,16 @@ public function testVisualSwatchDataValues() } } } + configurable_product_options_selection { + configurable_options { + values { + label + swatch { + value + } + } + } + } } } } @@ -123,5 +133,15 @@ public function testVisualSwatchDataValues() $option['values'][1]['swatch_data']['thumbnail'], $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_THUMBNAIL_NAME, $imageName) ); + + $configurableProductOptionsSelection = + $product['configurable_product_options_selection']['configurable_options'][0]; + + $this->assertArrayHasKey('values', $configurableProductOptionsSelection); + $this->assertEquals($color, $configurableProductOptionsSelection['values'][0]['swatch']['value']); + $this->assertStringContainsString( + $configurableProductOptionsSelection['values'][1]['swatch']['value'], + $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_IMAGE_NAME, $imageName) + ); } } From 0794361529e349d8f52620e083eb92bf3dc0121e Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 2 Mar 2021 16:05:31 -0600 Subject: [PATCH 089/137] PWA-1326: Implement the schema changes for Configurable Options Selection - add test for swatches --- .../Magento/GraphQl/Swatches/ProductSwatchDataTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php index 6c02882af8ecc..713a16a6bfaa9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php @@ -103,6 +103,9 @@ public function testVisualSwatchDataValues() label swatch { value + ... on ImageSwatchData { + thumbnail + } } } } @@ -143,5 +146,9 @@ public function testVisualSwatchDataValues() $configurableProductOptionsSelection['values'][1]['swatch']['value'], $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_IMAGE_NAME, $imageName) ); + $this->assertEquals( + $configurableProductOptionsSelection['values'][1]['swatch']['thumbnail'], + $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_THUMBNAIL_NAME, $imageName) + ); } } From ccd77b8cfea8c8a918094362508048515cd5c4d4 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 2 Mar 2021 23:53:41 -0600 Subject: [PATCH 090/137] MC-40538: When entering example.com/0 into browser the homepage is shown (example.com) --- .../Store/Model/Validation/StoreValidator.php | 7 +- .../App/Request/PathInfoProcessorTest.php | 141 +++---------- .../Request/StorePathInfoValidatorTest.php | 196 ++++++++++++++++++ .../Validation/StoreCodeValidatorTest.php | 82 ++++++++ .../Validation/StoreNameValidatorTest.php | 82 ++++++++ .../Model/Validation/StoreValidatorTest.php | 100 +++++++++ .../App/Request/PathInfoProcessorTest.php | 104 +++------- .../Model/Validation/StoreValidatorTest.php | 59 ++++++ 8 files changed, 584 insertions(+), 187 deletions(-) create mode 100644 app/code/Magento/Store/Test/Unit/App/Request/StorePathInfoValidatorTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/Validation/StoreCodeValidatorTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/Validation/StoreNameValidatorTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/Validation/StoreValidatorTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Store/Model/Validation/StoreValidatorTest.php diff --git a/app/code/Magento/Store/Model/Validation/StoreValidator.php b/app/code/Magento/Store/Model/Validation/StoreValidator.php index c98b5a4bea890..7fa969f7c3621 100644 --- a/app/code/Magento/Store/Model/Validation/StoreValidator.php +++ b/app/code/Magento/Store/Model/Validation/StoreValidator.php @@ -9,6 +9,7 @@ use Magento\Framework\Validator\AbstractValidator; use Magento\Framework\Validator\DataObjectFactory; +use Magento\Framework\Validator\ValidatorInterface; /** * Store model validator. @@ -21,15 +22,15 @@ class StoreValidator extends AbstractValidator private $dataObjectValidatorFactory; /** - * @var array + * @var ValidatorInterface[] */ private $rules; /** * @param DataObjectFactory $dataObjectValidatorFactory - * @param array $rules + * @param ValidatorInterface[] $rules */ - public function __construct(DataObjectFactory $dataObjectValidatorFactory, array $rules) + public function __construct(DataObjectFactory $dataObjectValidatorFactory, array $rules = []) { $this->dataObjectValidatorFactory = $dataObjectValidatorFactory; $this->rules = $rules; diff --git a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php index 93735031d2444..fd5da89a4523c 100644 --- a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php @@ -7,117 +7,81 @@ namespace Magento\Store\Test\Unit\App\Request; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; -use Magento\Framework\App\Request\PathInfo; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\App\Request\PathInfoProcessor; use Magento\Store\App\Request\StorePathInfoValidator; -use Magento\Store\Model\Store; -use Magento\Store\Model\Validation\StoreCodeValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class PathInfoProcessorTest extends TestCase { /** - * @var PathInfoProcessor - */ - private $model; - - /** - * @var MockObject|Http - */ - private $requestMock; - - /** - * @var MockObject|ScopeConfigInterface - */ - private $validatorConfigMock; - - /** - * @var MockObject|PathInfo + * @var StorePathInfoValidator|MockObject */ - private $pathInfoMock; + private $storePathInfoValidatorMock; /** - * @var MockObject|StoreCodeValidator + * @var PathInfoProcessor */ - private $storeCodeValidator; + private $model; /** - * @var MockObject|StoreRepositoryInterface + * @var Http|MockObject */ - private $storeRepositoryMock; + private $requestMock; /** - * @var StorePathInfoValidator + * @var string */ - private $storePathInfoValidator; + private $storeCode; /** * @var string */ - private $pathInfo = '/storeCode/node_one/'; + private $pathInfo; protected function setUp(): void { + $this->storePathInfoValidatorMock = $this->createMock(StorePathInfoValidator::class); + $this->model = new PathInfoProcessor($this->storePathInfoValidatorMock); + $this->requestMock = $this->createMock(Http::class); + $this->storeCode = 'storeCode'; + $this->pathInfo = '/' . $this->storeCode . '/node_one/'; + } - $this->validatorConfigMock = $this->createMock(ScopeConfigInterface::class); - $this->storeRepositoryMock = $this->createMock(StoreRepositoryInterface::class); - $this->pathInfoMock = $this->createMock(PathInfo ::class); - $this->storeCodeValidator = $this->createMock(StoreCodeValidator::class); + public function testProcessIfStoreIsEmpty(): void + { + $this->storePathInfoValidatorMock->expects($this->once()) + ->method('getValidStoreCode') + ->willReturn(null); - $this->storePathInfoValidator = new StorePathInfoValidator( - $this->validatorConfigMock, - $this->storeRepositoryMock, - $this->pathInfoMock, - $this->storeCodeValidator - ); - $this->model = new PathInfoProcessor( - $this->storePathInfoValidator - ); + $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); + $this->assertEquals($this->pathInfo, $pathInfo); } - public function testProcessIfStoreExistsAndIsNotDirectAccessToFrontName() + public function testProcessIfStoreExistsAndIsNotDirectAccessToFrontName(): void { - $this->validatorConfigMock->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn(true); - $this->storeCodeValidator->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - - $store = $this->createMock(Store::class); - $this->storeRepositoryMock->expects($this->once()) - ->method('getActiveStoreByCode') - ->with('storeCode') - ->willReturn($store); + $this->storePathInfoValidatorMock->expects($this->once()) + ->method('getValidStoreCode') + ->willReturn($this->storeCode); $this->requestMock->expects($this->atLeastOnce()) ->method('isDirectAccessFrontendName') - ->with('storeCode') + ->with($this->storeCode) ->willReturn(false); $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); $this->assertEquals('/node_one/', $pathInfo); } - public function testProcessIfStoreExistsAndDirectAccessToFrontName() + public function testProcessIfStoreExistsAndDirectAccessToFrontName(): void { - $this->validatorConfigMock->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn(true); - $this->storeCodeValidator->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - - $this->storeRepositoryMock->expects($this->once()) - ->method('getActiveStoreByCode'); + $this->storePathInfoValidatorMock->expects($this->once()) + ->method('getValidStoreCode') + ->willReturn($this->storeCode); $this->requestMock->expects($this->atLeastOnce()) ->method('isDirectAccessFrontendName') - ->with('storeCode') + ->with($this->storeCode) ->willReturn(true); $this->requestMock->expects($this->once()) ->method('setActionName') @@ -126,45 +90,4 @@ public function testProcessIfStoreExistsAndDirectAccessToFrontName() $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); $this->assertEquals($this->pathInfo, $pathInfo); } - - public function testProcessIfStoreIsEmpty() - { - $this->validatorConfigMock->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn(true); - $this->storeCodeValidator->expects($this->any()) - ->method('isValid') - ->willReturn(true); - - $path = '/0/node_one/'; - $this->storeRepositoryMock->expects($this->never()) - ->method('getActiveStoreByCode'); - $this->requestMock->expects($this->never()) - ->method('isDirectAccessFrontendName'); - $this->requestMock->expects($this->never()) - ->method('setActionName'); - - $pathInfo = $this->model->process($this->requestMock, $path); - $this->assertEquals($path, $pathInfo); - } - - public function testProcessIfStoreCodeIsNotExist() - { - $this->validatorConfigMock->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn(true); - $this->storeCodeValidator->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - - $this->storeRepositoryMock->expects($this->once()) - ->method('getActiveStoreByCode') - ->with('storeCode') - ->willThrowException(new NoSuchEntityException()); - $this->requestMock->expects($this->never()) - ->method('isDirectAccessFrontendName'); - - $pathInfo = $this->model->process($this->requestMock, $this->pathInfo); - $this->assertEquals($this->pathInfo, $pathInfo); - } } diff --git a/app/code/Magento/Store/Test/Unit/App/Request/StorePathInfoValidatorTest.php b/app/code/Magento/Store/Test/Unit/App/Request/StorePathInfoValidatorTest.php new file mode 100644 index 0000000000000..5c2b1fb2f6f38 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/App/Request/StorePathInfoValidatorTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\App\Request; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Request\PathInfo; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\App\Request\StorePathInfoValidator; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreIsInactiveException; +use Magento\Store\Model\Validation\StoreCodeValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class StorePathInfoValidatorTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $configMock; + + /** + * @var StoreRepositoryInterface|MockObject + */ + private $storeRepositoryMock; + + /** + * @var PathInfo|MockObject + */ + private $pathInfoMock; + + /** + * @var StoreCodeValidator|MockObject + */ + private $storeCodeValidatorMock; + + /** + * @var Http|MockObject + */ + private $requestMock; + + /** + * @var StorePathInfoValidator + */ + private $storePathInfoValidator; + + protected function setUp(): void + { + $this->configMock = $this->createMock(ScopeConfigInterface::class); + $this->storeRepositoryMock = $this->createMock(StoreRepositoryInterface::class); + $this->pathInfoMock = $this->createMock(PathInfo::class); + $this->storeCodeValidatorMock = $this->createMock(StoreCodeValidator::class); + $this->storePathInfoValidator = new StorePathInfoValidator( + $this->configMock, + $this->storeRepositoryMock, + $this->pathInfoMock, + $this->storeCodeValidatorMock + ); + + $this->requestMock = $this->createMock(Http::class); + $this->requestMock->method('getRequestUri') + ->willReturn('/path/'); + $this->requestMock->method('getBaseUrl') + ->willReturn('example.com'); + } + + public function testGetValidStoreCodeWithoutStoreInUrl(): void + { + $this->pathInfoMock->method('getPathInfo') + ->willReturn('/a/b/'); + $this->storeCodeValidatorMock->method('isValid') + ->willReturn(true); + + $this->configMock->expects($this->once()) + ->method('getValue') + ->with(Store::XML_PATH_STORE_IN_URL) + ->willReturn(false); + $this->storeRepositoryMock->expects($this->never()) + ->method('getActiveStoreByCode'); + + $result = $this->storePathInfoValidator->getValidStoreCode($this->requestMock, '/b/c/'); + $this->assertNull($result); + } + + public function testGetValidStoreCodeWithoutPathInfo(): void + { + $storeCode = 'store1'; + + $this->configMock->expects($this->once()) + ->method('getValue') + ->with(Store::XML_PATH_STORE_IN_URL) + ->willReturn(true); + $this->pathInfoMock->expects($this->once()) + ->method('getPathInfo') + ->willReturn('/' . $storeCode . '/path1/'); + $this->storeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with($storeCode) + ->willReturn(true); + $store = $this->createMock(Store::class); + $this->storeRepositoryMock->expects($this->once()) + ->method('getActiveStoreByCode') + ->with($storeCode) + ->willReturn($store); + + $result = $this->storePathInfoValidator->getValidStoreCode($this->requestMock, ''); + $this->assertEquals($storeCode, $result); + } + + public function testGetValidStoreCodeWithEmptyPathInfo(): void + { + $this->configMock->expects($this->once()) + ->method('getValue') + ->with(Store::XML_PATH_STORE_IN_URL) + ->willReturn(true); + $this->pathInfoMock->expects($this->once()) + ->method('getPathInfo') + ->willReturn(''); + $this->storeCodeValidatorMock->method('isValid') + ->willReturn(true); + $this->storeRepositoryMock->expects($this->never()) + ->method('getActiveStoreByCode'); + + $result = $this->storePathInfoValidator->getValidStoreCode($this->requestMock, ''); + $this->assertNull($result); + } + + /** + * @dataProvider getValidStoreCodeExceptionDataProvider + * @param \Throwable $exception + */ + public function testGetValidStoreCodeThrowsException(\Throwable $exception): void + { + $this->configMock->method('getValue') + ->with(Store::XML_PATH_STORE_IN_URL) + ->willReturn(true); + $this->storeCodeValidatorMock->method('isValid') + ->willReturn(true); + + $this->storeRepositoryMock->expects($this->once()) + ->method('getActiveStoreByCode') + ->willThrowException($exception); + + $result = $this->storePathInfoValidator->getValidStoreCode($this->requestMock, '/store/'); + $this->assertNull($result); + } + + public function getValidStoreCodeExceptionDataProvider(): array + { + return [ + [new NoSuchEntityException()], + [new StoreIsInactiveException()], + ]; + } + + /** + * @dataProvider getValidStoreCodeDataProvider + * @param string $pathInfo + * @param bool $isStoreCodeValid + * @param string|null $expectedResult + */ + public function testGetValidStoreCode(string $pathInfo, bool $isStoreCodeValid, ?string $expectedResult): void + { + $this->configMock->method('getValue') + ->with(Store::XML_PATH_STORE_IN_URL) + ->willReturn(true); + $this->pathInfoMock->method('getPathInfo') + ->willReturn('/store2/path2/'); + $this->storeCodeValidatorMock->method('isValid') + ->willReturn($isStoreCodeValid); + $store = $this->createMock(Store::class); + $this->storeRepositoryMock->method('getActiveStoreByCode') + ->willReturn($store); + + $result = $this->storePathInfoValidator->getValidStoreCode($this->requestMock, $pathInfo); + $this->assertEquals($expectedResult, $result); + } + + public function getValidStoreCodeDataProvider(): array + { + return [ + ['store1', true, 'store1'], + ['/store1/path1/', true, 'store1'], + ['/', true, null], + ['admin', true, null], + ['1', false, null], + ]; + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/Validation/StoreCodeValidatorTest.php b/app/code/Magento/Store/Test/Unit/Model/Validation/StoreCodeValidatorTest.php new file mode 100644 index 0000000000000..7dbb3e3227bfb --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/Validation/StoreCodeValidatorTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\Validation; + +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; +use Magento\Store\Model\Validation\StoreCodeValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class StoreCodeValidatorTest extends TestCase +{ + /** + * @var RegexFactory|MockObject + */ + private $regexValidatorFactoryMock; + + /** + * @var Regex|MockObject + */ + private $regexValidatorMock; + + /** + * @var StoreCodeValidator + */ + private $model; + + protected function setUp(): void + { + $this->regexValidatorFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->regexValidatorMock = $this->createMock(Regex::class); + $this->regexValidatorFactoryMock->method('create') + ->willReturn($this->regexValidatorMock); + + $this->model = new StoreCodeValidator($this->regexValidatorFactoryMock); + } + + /** + * @dataProvider isValidDataProvider + * @param string $value + * @param bool $isValid + * @param array $messages + */ + public function testIsValid(string $value, bool $isValid, array $messages): void + { + $this->regexValidatorMock->expects($this->once()) + ->method('isValid') + ->with($value) + ->willReturn($isValid); + $this->regexValidatorMock->expects($this->once()) + ->method('getMessages') + ->willReturn($messages); + + $result = $this->model->isValid($value); + $this->assertEquals($isValid, $result); + $this->assertEquals($messages, $this->model->getMessages()); + } + + public function isValidDataProvider(): array + { + return [ + 'true' => [ + 'abc', + true, + [] + ], + 'false' => [ + '5', + false, + ['code is not valid'] + ], + ]; + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/Validation/StoreNameValidatorTest.php b/app/code/Magento/Store/Test/Unit/Model/Validation/StoreNameValidatorTest.php new file mode 100644 index 0000000000000..42caa124b13bb --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/Validation/StoreNameValidatorTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\Validation; + +use Magento\Framework\Validator\NotEmpty; +use Magento\Framework\Validator\NotEmptyFactory; +use Magento\Store\Model\Validation\StoreNameValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class StoreNameValidatorTest extends TestCase +{ + /** + * @var NotEmptyFactory|MockObject + */ + private $notEmptyValidatorFactoryMock; + + /** + * @var NotEmpty|MockObject + */ + private $notEmptyValidatorMock; + + /** + * @var StoreNameValidator + */ + private $model; + + protected function setUp(): void + { + $this->notEmptyValidatorFactoryMock = $this->getMockBuilder(NotEmptyFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->notEmptyValidatorMock = $this->createMock(NotEmpty::class); + $this->notEmptyValidatorFactoryMock->method('create') + ->willReturn($this->notEmptyValidatorMock); + + $this->model = new StoreNameValidator($this->notEmptyValidatorFactoryMock); + } + + /** + * @dataProvider isValidDataProvider + * @param string $value + * @param bool $isValid + * @param array $messages + */ + public function testIsValid(string $value, bool $isValid, array $messages): void + { + $this->notEmptyValidatorMock->expects($this->once()) + ->method('isValid') + ->with($value) + ->willReturn($isValid); + $this->notEmptyValidatorMock->expects($this->once()) + ->method('getMessages') + ->willReturn($messages); + + $result = $this->model->isValid($value); + $this->assertEquals($isValid, $result); + $this->assertEquals($messages, $this->model->getMessages()); + } + + public function isValidDataProvider(): array + { + return [ + 'true' => [ + 'Name1', + true, + [] + ], + 'false' => [ + '', + false, + ['name is not valid'] + ], + ]; + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/Validation/StoreValidatorTest.php b/app/code/Magento/Store/Test/Unit/Model/Validation/StoreValidatorTest.php new file mode 100644 index 0000000000000..7b089939c1580 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/Validation/StoreValidatorTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\Validation; + +use Magento\Framework\Validator\DataObject; +use Magento\Framework\Validator\DataObjectFactory; +use Magento\Framework\Validator\ValidatorInterface; +use Magento\Store\Model\Validation\StoreValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class StoreValidatorTest extends TestCase +{ + /** + * @var DataObjectFactory|MockObject + */ + private $dataObjectValidatorFactoryMock; + + /** + * @var DataObject|MockObject + */ + private $dataObjectValidatorMock; + + /** + * @var array + */ + private $ruleMocks; + + /** + * @var StoreValidator + */ + private $model; + + protected function setUp(): void + { + $this->dataObjectValidatorFactoryMock = $this->getMockBuilder(DataObjectFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->dataObjectValidatorMock = $this->createMock(DataObject::class); + $this->dataObjectValidatorFactoryMock->method('create') + ->willReturn($this->dataObjectValidatorMock); + $ruleMock1 = $this->createMock(ValidatorInterface::class); + $ruleMock2 = $this->createMock(ValidatorInterface::class); + $this->ruleMocks = [ + [$ruleMock1, 'field1'], + [$ruleMock2, 'field2'], + ]; + + $this->model = new StoreValidator( + $this->dataObjectValidatorFactoryMock, + array_combine(array_column($this->ruleMocks, 1), array_column($this->ruleMocks, 0)) + ); + } + + /** + * @dataProvider isValidDataProvider + * @param \Magento\Framework\DataObject $value + * @param bool $isValid + * @param array $messages + */ + public function testIsValid(\Magento\Framework\DataObject $value, bool $isValid, array $messages): void + { + $this->dataObjectValidatorMock->expects($this->exactly(count($this->ruleMocks))) + ->method('addRule') + ->withConsecutive(...$this->ruleMocks); + $this->dataObjectValidatorMock->expects($this->once()) + ->method('isValid') + ->with($value) + ->willReturn($isValid); + $this->dataObjectValidatorMock->expects($this->once()) + ->method('getMessages') + ->willReturn($messages); + + $result = $this->model->isValid($value); + $this->assertEquals($isValid, $result); + $this->assertEquals($messages, $this->model->getMessages()); + } + + public function isValidDataProvider(): array + { + return [ + 'true' => [ + new \Magento\Framework\DataObject(['field1' => 'value1', 'field2' => 'value2']), + true, + [] + ], + 'false' => [ + new \Magento\Framework\DataObject(), + false, + ['store is not valid'] + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php b/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php index a7662dadac050..a58cc15b51500 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php @@ -5,89 +5,72 @@ */ namespace Magento\Store\App\Request; -use \Magento\TestFramework\Helper\Bootstrap; -use \Magento\Store\Model\ScopeInterface; -use \Magento\Store\Model\Store; +use Magento\Framework\App\RequestInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; -class PathInfoProcessorTest extends \PHPUnit\Framework\TestCase +class PathInfoProcessorTest extends TestCase { /** - * @var \Magento\Store\App\Request\PathInfoProcessor + * @var PathInfoProcessor */ private $pathProcessor; protected function setUp(): void { - $this->pathProcessor = Bootstrap::getObjectManager()->create( - \Magento\Store\App\Request\PathInfoProcessor::class - ); + $this->pathProcessor = Bootstrap::getObjectManager()->create(PathInfoProcessor::class); } /** * @covers \Magento\Store\App\Request\PathInfoProcessor::process + * @magentoConfigFixture web/url/use_store 1 * @dataProvider notValidStoreCodeDataProvider + * @param string $pathInfo */ - public function testProcessNotValidStoreCode($pathInfo) + public function testProcessNotValidStoreCode(string $pathInfo) { - /** @var \Magento\Framework\App\RequestInterface $request */ - $request = Bootstrap::getObjectManager()->create(\Magento\Framework\App\RequestInterface::class); - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true); + $request = Bootstrap::getObjectManager()->create(RequestInterface::class); $info = $this->pathProcessor->process($request, $pathInfo); $this->assertEquals($pathInfo, $info); } - public function notValidStoreCodeDataProvider() + public function notValidStoreCodeDataProvider(): array { return [ - ['not_valid_store_code_int' => '/100500/m/c/a'], - ['not_valid_store_code_str' => '/test_string/m/c/a'], + ['default store id' => '/0/m/c/a'], + ['main store id' => '/1/m/c/a'], + ['nonexistent store code' => '/test_string/m/c/a'], + ['admin store code' => '/admin/m/c/a'], + ['empty path' => '/'], ]; } /** * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoConfigFixture web/url/use_store 1 */ public function testProcessValidStoreCodeCaseProcessStoreName() { - /** @var \Magento\Store\Model\Store $store */ - $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); - - /** @var \Magento\Framework\App\RequestInterface $request */ - $request = Bootstrap::getObjectManager()->create(\Magento\Framework\App\RequestInterface::class); - - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true, ScopeInterface::SCOPE_STORE, $store->getCode()); - $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); + $storeCode = 'fixturestore'; + $request = Bootstrap::getObjectManager()->create(RequestInterface::class); + $pathInfo = sprintf('/%s/m/c/a', $storeCode); $this->assertEquals('/m/c/a', $this->pathProcessor->process($request, $pathInfo)); } /** * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoConfigFixture web/url/use_store 1 */ public function testProcessValidStoreCodeWhenStoreIsDirectFrontNameWithFrontName() { - /** @var \Magento\Store\Model\Store $store */ - $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); - - /** @var \Magento\Framework\App\RequestInterface $request */ + $storeCode = 'fixturestore'; $request = Bootstrap::getObjectManager()->create( - \Magento\Framework\App\RequestInterface::class, - ['directFrontNames' => [$store->getCode() => true]] + RequestInterface::class, + ['directFrontNames' => [$storeCode => true]] ); - - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true, ScopeInterface::SCOPE_STORE, $store->getCode()); - $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); + $pathInfo = sprintf('/%s/m/c/a', $storeCode); $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); $this->assertEquals(\Magento\Framework\App\Router\Base::NO_ROUTE, $request->getActionName()); } @@ -95,42 +78,13 @@ public function testProcessValidStoreCodeWhenStoreIsDirectFrontNameWithFrontName /** * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php - */ - public function testProcessValidStoreCodeWhenStoreCodeisAdmin() - { - /** @var \Magento\Store\Model\Store $store */ - $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); - - /** @var \Magento\Framework\App\RequestInterface $request */ - $request = Bootstrap::getObjectManager()->create( - \Magento\Framework\App\RequestInterface::class, - ['directFrontNames' => ['someFrontName' => true]] - ); - - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, true); - $config->setValue(Store::XML_PATH_STORE_IN_URL, false, ScopeInterface::SCOPE_STORE, $store->getCode()); - $pathInfo = sprintf('/%s/m/c/a', 'admin'); - $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); - } - - /** - * @covers \Magento\Store\App\Request\PathInfoProcessor::process + * @magentoConfigFixture web/url/use_store 0 */ public function testProcessValidStoreCodeWhenUrlConfigIsDisabled() { - /** @var \Magento\Framework\App\RequestInterface $request */ - $request = Bootstrap::getObjectManager()->create( - \Magento\Framework\App\RequestInterface::class, - ['directFrontNames' => ['someFrontName' => true]] - ); - - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->setValue(Store::XML_PATH_STORE_IN_URL, false); - $pathInfo = sprintf('/%s/m/c/a', 'whatever'); + $storeCode = 'fixturestore'; + $request = Bootstrap::getObjectManager()->create(RequestInterface::class); + $pathInfo = sprintf('/%s/m/c/a', $storeCode); $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); $this->assertNull($request->getActionName()); } diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/Validation/StoreValidatorTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/Validation/StoreValidatorTest.php new file mode 100644 index 0000000000000..f707ac8cd14c4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/Model/Validation/StoreValidatorTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Validation; + +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class StoreValidatorTest extends TestCase +{ + /** + * @var StoreValidator + */ + private $storeValidator; + + protected function setUp(): void + { + $this->storeValidator = Bootstrap::getObjectManager()->create(StoreValidator::class); + } + + /** + * @dataProvider isValidDataProvider + * @param Store $store + * @param bool $isValid + */ + public function testIsValid(Store $store, bool $isValid): void + { + $result = $this->storeValidator->isValid($store); + $this->assertEquals($isValid, $result); + } + + public function isValidDataProvider(): array + { + $validStore = Bootstrap::getObjectManager()->create(Store::class); + $validStore->setName('name'); + $validStore->setCode('code'); + $emptyStore = Bootstrap::getObjectManager()->create(Store::class); + $storeWithEmptyName = Bootstrap::getObjectManager()->create(Store::class); + $storeWithEmptyName->setCode('code'); + $storeWithEmptyCode = Bootstrap::getObjectManager()->create(Store::class); + $storeWithEmptyCode->setName('name'); + $storeWithInvalidCode = Bootstrap::getObjectManager()->create(Store::class); + $storeWithInvalidCode->setName('name'); + $storeWithInvalidCode->setCode('5'); + + return [ + [$validStore, true], + [$emptyStore, false], + [$storeWithEmptyName, false], + [$storeWithEmptyCode, false], + [$storeWithInvalidCode, false], + ]; + } +} From 025c8c69dacdaf1b82ffba23674df1e75207403a Mon Sep 17 00:00:00 2001 From: Viktor Petryk <victor.petryk@transoftgroup.com> Date: Wed, 3 Mar 2021 10:30:49 +0200 Subject: [PATCH 091/137] MC-32805: Error message appears when update qty item in the cart. --- .../Controller/Cart/UpdateItemOptions.php | 44 +++--- .../Controller/Cart/UpdateItemOptionsTest.php | 127 ++++++++++++++++++ 2 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemOptionsTest.php diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php index 40ce2252581cf..205f0aa8dbd2e 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php @@ -1,20 +1,29 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Checkout\Controller\Cart; +use Magento\Checkout\Controller\Cart; +use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Locale\ResolverInterface; +use Psr\Log\LoggerInterface; -class UpdateItemOptions extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface +/** + * Process updating product options in a cart item. + */ +class UpdateItemOptions extends Cart implements HttpPostActionInterface { /** - * Update product configuration for a cart item + * Update product configuration for a cart item. * - * @return \Magento\Framework\Controller\Result\Redirect + * @return Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -28,27 +37,27 @@ public function execute() } try { if (isset($params['qty'])) { - $filter = new \Zend_Filter_LocalizedToNormalized( - ['locale' => $this->_objectManager->get( - \Magento\Framework\Locale\ResolverInterface::class - )->getLocale()] + $inputFilter = new \Zend_Filter_LocalizedToNormalized( + [ + 'locale' => $this->_objectManager->get(ResolverInterface::class)->getLocale(), + ] ); - $params['qty'] = $filter->filter($params['qty']); + $params['qty'] = $inputFilter->filter($params['qty']); } $quoteItem = $this->cart->getQuote()->getItemById($id); if (!$quoteItem) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("The quote item isn't found. Verify the item and try again.") ); } - $item = $this->cart->updateItem($id, new \Magento\Framework\DataObject($params)); + $item = $this->cart->updateItem($id, new DataObject($params)); if (is_string($item)) { - throw new \Magento\Framework\Exception\LocalizedException(__($item)); + throw new LocalizedException(__($item)); } if ($item->getHasError()) { - throw new \Magento\Framework\Exception\LocalizedException(__($item->getMessage())); + throw new LocalizedException(__($item->getMessage())); } $related = $this->getRequest()->getParam('related_product'); @@ -73,7 +82,7 @@ public function execute() } return $this->_goBack($this->_url->getUrl('checkout/cart')); } - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { if ($this->_checkoutSession->getUseNotice(true)) { $this->messageManager->addNoticeMessage($e->getMessage()); } else { @@ -87,14 +96,15 @@ public function execute() if ($url) { return $this->resultRedirectFactory->create()->setUrl($url); } else { - $cartUrl = $this->_objectManager->get(\Magento\Checkout\Helper\Cart::class)->getCartUrl(); - return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl($cartUrl)); + $cartUrl = $this->_objectManager->get(CartHelper::class)->getCartUrl(); + return $this->resultRedirectFactory->create()->setUrl($cartUrl); } } catch (\Exception $e) { $this->messageManager->addExceptionMessage($e, __('We can\'t update the item right now.')); - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->_objectManager->get(LoggerInterface::class)->critical($e); return $this->_goBack(); } + return $this->resultRedirectFactory->create()->setPath('*/*'); } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemOptionsTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemOptionsTest.php new file mode 100644 index 0000000000000..c7765b2d663f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemOptionsTest.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Controller\Cart; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Integration tests for \Magento\Checkout\Controller\Cart\UpdateItemOptions class. + */ +class UpdateItemOptionsTest extends AbstractController +{ + /** + * @var FormKey + */ + private $formKey; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->formKey = $this->_objectManager->get(FormKey::class); + $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Tests that product is successfully updated in the shopping cart. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product.php + */ + public function testUpdateProductOptionsInQuote() + { + $product = $this->productRepository->get('simple'); + $quoteItem = $this->checkoutSession->getQuote()->getItemByProduct($product); + $postData = $this->preparePostData($product, $quoteItem); + $this->dispatchUpdateItemOptionsRequest($postData); + $this->assertTrue($this->getResponse()->isRedirect()); + $this->assertRedirect($this->stringContains('/checkout/cart/')); + $message = (string)__( + '%1 was updated in your shopping cart.', + $product->getName() + ); + $this->assertSessionMessages( + $this->containsEqual($message), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Tests that product can't be updated with an empty shopping cart. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product.php + */ + public function testUpdateProductOptionsWithEmptyQuote() + { + $product = $this->productRepository->get('simple'); + $quoteItem = $this->checkoutSession->getQuote()->getItemByProduct($product); + $postData = $this->preparePostData($product, $quoteItem); + $this->checkoutSession->clearQuote(); + $this->dispatchUpdateItemOptionsRequest($postData); + $this->assertTrue($this->getResponse()->isRedirect()); + $this->assertRedirect($this->stringContains('/checkout/cart/')); + $message = (string)__('The quote item isn't found. Verify the item and try again.'); + $this->assertSessionMessages( + $this->containsEqual($message), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Prepare post data for the request. + * + * @param ProductInterface $product + * @param QuoteItem|bool $quoteItem + * @return array + */ + private function preparePostData(ProductInterface $product, $quoteItem): array + { + return [ + 'product' => $product->getId(), + 'selected_configurable_option' => '', + 'related_product' => '', + 'item' => $quoteItem->getId(), + 'form_key' => $this->formKey->getFormKey(), + 'qty' => '2', + ]; + } + + /** + * Perform request for updating product options in a quote item. + * + * @param array $postData + * @return void + */ + private function dispatchUpdateItemOptionsRequest(array $postData): void + { + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('checkout/cart/updateItemOptions/id/' . $postData['item']); + } +} From a814887ebb3cf3eb9da85ef64b9cfe9d74d9c0d7 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 3 Mar 2021 14:47:08 +0200 Subject: [PATCH 092/137] MQE-2539: Create automated test for: "Set Custom Prices in Shared Catalog for multiple websites" --- app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 5375459122e69..6e0682c7ff3e8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -1392,6 +1392,15 @@ <entity name="SimpleProductUpdatePrice16" type="product2"> <data key="price">16.00</data> </entity> + <entity name="SimpleProductUpdatePrice90" type="product2"> + <data key="price">90.00</data> + </entity> + <entity name="SimpleProductUpdatePrice95" type="product2"> + <data key="price">95.00</data> + </entity> + <entity name="SimpleProductUpdatePrice80" type="product2"> + <data key="price">80.00</data> + </entity> <entity name="ProductWithTwoTextFieldOptions" type="product"> <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionField</requiredEntity> From 14558892cc0aa06629e0e7b0a78276587fcc6d6d Mon Sep 17 00:00:00 2001 From: engcom-Kilo <grp-engcom-vendorworker-Kilo@adobe.com> Date: Tue, 2 Mar 2021 15:35:58 +0200 Subject: [PATCH 093/137] MC-40654: Some strings not translatable in M2.4.1 --- lib/internal/Magento/Framework/App/FrontController.php | 7 ++++--- .../Framework/App/Test/Unit/FrontControllerTest.php | 9 ++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/internal/Magento/Framework/App/FrontController.php b/lib/internal/Magento/Framework/App/FrontController.php index 2f96b24176943..f80d17c072f94 100644 --- a/lib/internal/Magento/Framework/App/FrontController.php +++ b/lib/internal/Magento/Framework/App/FrontController.php @@ -184,6 +184,10 @@ private function processRequest( //Validating a request only once. if (!$this->validatedRequest) { + $area = $this->areaList->getArea($this->appState->getAreaCode()); + $area->load(Area::PART_DESIGN); + $area->load(Area::PART_TRANSLATE); + try { $this->requestValidator->validate($request, $actionInstance); } catch (InvalidRequestException $exception) { @@ -193,9 +197,6 @@ private function processRequest( ["exception" => $exception] ); $result = $exception->getReplaceResult(); - $area = $this->areaList->getArea($this->appState->getAreaCode()); - $area->load(Area::PART_DESIGN); - $area->load(Area::PART_TRANSLATE); if ($messages = $exception->getMessages()) { foreach ($messages as $message) { $this->messages->addErrorMessage($message); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/FrontControllerTest.php b/lib/internal/Magento/Framework/App/Test/Unit/FrontControllerTest.php index 20da38100b0f4..fdb109ce30708 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/FrontControllerTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/FrontControllerTest.php @@ -226,7 +226,10 @@ public function testDispatched() $this->routerList->expects($this->any()) ->method('current') ->willReturn($this->router); - + $this->appStateMock->expects($this->any())->method('getAreaCode')->willReturn('frontend'); + $this->areaMock->expects($this->at(0))->method('load')->with(Area::PART_DESIGN)->willReturnSelf(); + $this->areaMock->expects($this->at(1))->method('load')->with(Area::PART_TRANSLATE)->willReturnSelf(); + $this->areaListMock->expects($this->any())->method('getArea')->willReturn($this->areaMock); $this->request->expects($this->at(0))->method('isDispatched')->willReturn(false); $this->request->expects($this->at(1))->method('setDispatched')->with(true); $this->request->expects($this->at(2))->method('isDispatched')->willReturn(true); @@ -261,6 +264,10 @@ public function testDispatchedNotFoundException() ->method('current') ->willReturn($this->router); + $this->appStateMock->expects($this->any())->method('getAreaCode')->willReturn('frontend'); + $this->areaMock->expects($this->at(0))->method('load')->with(Area::PART_DESIGN)->willReturnSelf(); + $this->areaMock->expects($this->at(1))->method('load')->with(Area::PART_TRANSLATE)->willReturnSelf(); + $this->areaListMock->expects($this->any())->method('getArea')->willReturn($this->areaMock); $this->request->expects($this->at(0))->method('isDispatched')->willReturn(false); $this->request->expects($this->at(1))->method('initForward'); $this->request->expects($this->at(2))->method('setActionName')->with('noroute'); From 1ac7ca68fd3d7d66ccc7a10deb620fbfb1513250 Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Wed, 3 Mar 2021 09:54:24 -0600 Subject: [PATCH 094/137] B2B-1632: Add MFTF test for MC-38948 - Addressing CR feedback --- app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php | 4 ++-- .../Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php index 96b2d77c951b8..53cb65e9fed5d 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php +++ b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php @@ -158,7 +158,7 @@ public function assertFileExists($filePath, $message = ''): void * * @throws \Magento\Framework\Exception\FileSystemException */ - public function assertSearchedForFileExists($path, $pattern, $message = ""): void + public function assertGlobbedFileExists($path, $pattern, $message = ""): void { $files = $this->driver->search($pattern, $path); $this->assertNotEmpty($files, $message); @@ -233,7 +233,7 @@ public function assertFileContainsString($filePath, $text, $message = ""): void * * @throws \Magento\Framework\Exception\FileSystemException */ - public function assertSearchedForFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void + public function assertGlobbedFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void { $files = $this->driver->search($pattern, $path); $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex]), $message); diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php index cb12f6a731bb1..530abb1ccbd46 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php +++ b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php @@ -143,7 +143,7 @@ public function assertFileExists($filePath, $message = ''): void * * @throws \Magento\Framework\Exception\FileSystemException */ - public function assertSearchedForFileExists($path, $pattern, $message = ""): void + public function assertGlobbedFileExists($path, $pattern, $message = ""): void { $realPath = $this->expandPath($path); $files = $this->driver->search($pattern, $realPath); @@ -223,7 +223,7 @@ public function assertFileContainsString($filePath, $text, $message = ""): void * * @throws \Magento\Framework\Exception\FileSystemException */ - public function assertSearchedForFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void + public function assertGlobbedFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void { $realPath = $this->expandPath($path); $files = $this->driver->search($pattern, $realPath); From 092cdf780f16159052db9d31ec886fe7542c5515 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 3 Mar 2021 14:35:03 -0600 Subject: [PATCH 095/137] PWA-1326: Implement the schema changes for Configurable Options Selection - configurable product suggest --- app/code/Magento/SwatchesGraphQl/composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 8b6651f89f3b7..5b50a10abfe2b 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -7,7 +7,10 @@ "magento/framework": "*", "magento/module-swatches": "*", "magento/module-catalog": "*", - "magento/module-catalog-graph-ql": "*", + "magento/module-catalog-graph-ql": "*" + + }, + "suggest": { "magento/module-configurable-product-graph-ql": "*" }, "license": [ From fbc560f7326086820331215f9d6b8e316ab4cfae Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 4 Mar 2021 14:04:11 +0200 Subject: [PATCH 096/137] MC-23989: Mini cart missing when you edit inline welcome message for guest and use special characters --- ...refrontCustomWelcomeMessageActionGroup.xml | 22 +++++ .../Test/Mftf/Data/TranslationData.xml | 7 ++ ...torefrontPanelHeaderTranslationSection.xml | 14 +++ ...tInlineTranslationWithQuoteSymbolsTest.xml | 95 +++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomWelcomeMessageActionGroup.xml create mode 100644 app/code/Magento/Translation/Test/Mftf/Section/StorefrontPanelHeaderTranslationSection.xml create mode 100644 app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomWelcomeMessageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomWelcomeMessageActionGroup.xml new file mode 100644 index 0000000000000..966529d280cfb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomWelcomeMessageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontCustomWelcomeMessageActionGroup"> + <annotations> + <description>Validates that the custom Welcome message is present on storefront header.</description> + </annotations> + <arguments> + <argument name="customMessage" type="string" defaultValue="Welcome to "Food & Drinks" store"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="waitForWelcomeMessage"/> + <see userInput="{{customMessage}}" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="verifyCustomMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Translation/Test/Mftf/Data/TranslationData.xml b/app/code/Magento/Translation/Test/Mftf/Data/TranslationData.xml index 4f787a52ba093..0b356cf9a73cb 100644 --- a/app/code/Magento/Translation/Test/Mftf/Data/TranslationData.xml +++ b/app/code/Magento/Translation/Test/Mftf/Data/TranslationData.xml @@ -171,4 +171,11 @@ <data key="original">Shipping Method:</data> <data key="custom">Shipping Method:</data> </entity> + <entity name="RevertWelcomeMessageTranslate" extends="CustomTranslationData"> + <requiredEntity type="translation_operation_translate">RevertWelcomeMessageTranslateData</requiredEntity> + </entity> + <entity name="RevertWelcomeMessageTranslateData" type="translation_operation_translate"> + <data key="original">Default welcome msg!</data> + <data key="custom">Default welcome msg!</data> + </entity> </entities> diff --git a/app/code/Magento/Translation/Test/Mftf/Section/StorefrontPanelHeaderTranslationSection.xml b/app/code/Magento/Translation/Test/Mftf/Section/StorefrontPanelHeaderTranslationSection.xml new file mode 100644 index 0000000000000..f58d24dfa1af7 --- /dev/null +++ b/app/code/Magento/Translation/Test/Mftf/Section/StorefrontPanelHeaderTranslationSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontPanelHeaderTranslationSection"> + <element name="welcomeMessage" type="text" selector="header>.panel .greet.welcome span" /> + </section> +</sections> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml new file mode 100644 index 0000000000000..f0beab9d1368c --- /dev/null +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontInlineTranslationWithQuoteSymbolsTest"> + <annotations> + <features value="Translation"/> + <stories value="Inline Translation"/> + <title value="Inline translation with quote symbols"/> + <description value="As merchant I want to be able to rename text labels using quote symbols in it"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-"/> + <useCaseId value="MC-23989"/> + <group value="translation"/> + <group value="developer_mode_only"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <magentoCLI command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" stepKey="enableTranslateInlineStorefront"/> + <createData entity="RevertWelcomeMessageTranslate" stepKey="revertWelcomeMessageTranslation"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <createData entity="SimpleProduct2" stepKey="createProductSecond"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheAfterTranslateEnabled"> + <argument name="tags" value=""/> + </actionGroup> + </before> + + <after> + <magentoCLI command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" stepKey="enableTranslateInlineStorefront"/> + <createData entity="RevertWelcomeMessageTranslate" stepKey="revertWelcomeMessageTranslation"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createProductSecond" stepKey="deleteProductSecond"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheAfterTranslateDisabled"> + <argument name="tags" value=""/> + </actionGroup> + </after> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontDefaultWelcomeMessageActionGroup" stepKey="assertDefaultWelcomeMessage"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="AssertOneProductNameInMiniCartActionGroup" stepKey="seeProductInMiniCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + + <actionGroup ref="AssertElementInTranslateInlineModeActionGroup" stepKey="assertWelcomeMessageInInlineTranslateMode"> + <argument name="elementSelector" value="{{StorefrontPanelHeaderTranslationSection.welcomeMessage}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenInlineTranslationPopupActionGroup" stepKey="openWelcomeMessageInlineTranslatePopup"> + <argument name="elementSelector" value="{{StorefrontPanelHeaderTranslationSection.welcomeMessage}}"/> + </actionGroup> + <actionGroup ref="StorefrontFillCustomTranslationFieldActionGroup" stepKey="fillInlineTranslateNewValue"> + <argument name="translateText" value="Welcome to "Food & Drinks" store"/> + </actionGroup> + <actionGroup ref="StorefrontSubmitInlineTranslationFormActionGroup" stepKey="saveInlineTranslateNewValue"/> + + <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheAfterTranslateDisabled"> + <argument name="tags" value=""/> + </actionGroup> + + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <actionGroup ref="AssertStorefrontCustomWelcomeMessageActionGroup" stepKey="verifyTranslatedWelcomeMessage"/> + <actionGroup ref="AssertOneProductNameInMiniCartActionGroup" stepKey="seeProductInMiniCartAgain"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openSecondProductPage"> + <argument name="productUrl" value="$createProductSecond.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontCustomWelcomeMessageActionGroup" stepKey="verifyTranslatedWelcomeMessageForSecondProduct"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addSecondProductToCart"> + <argument name="productName" value="$createProductSecond.name$"/> + </actionGroup> + <actionGroup ref="AssertOneProductNameInMiniCartActionGroup" stepKey="seeSecondProductInMiniCart"> + <argument name="productName" value="$createProductSecond.name$"/> + </actionGroup> + </test> +</tests> From eaa81fc127e4f8cecb88d62e7047adf7cbf619ae Mon Sep 17 00:00:00 2001 From: engcom-Kilo <grp-engcom-vendorworker-Kilo@adobe.com> Date: Thu, 4 Mar 2021 14:20:59 +0200 Subject: [PATCH 097/137] MC-40663: \Error object not handled properly in numerous areas. --- .../Cms/Controller/Adminhtml/Page/Save.php | 44 ++++++++------- .../Controller/Adminhtml/Page/SaveTest.php | 54 ++++++++++--------- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php index 449fdb4224a57..34b1e949271d7 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php @@ -3,13 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Page; -use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; use Magento\Cms\Model\Page; +use Magento\Cms\Model\PageFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Exception\LocalizedException; /** @@ -17,7 +24,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface +class Save extends Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -37,12 +44,12 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac protected $dataPersistor; /** - * @var \Magento\Cms\Model\PageFactory + * @var PageFactory */ private $pageFactory; /** - * @var \Magento\Cms\Api\PageRepositoryInterface + * @var PageRepositoryInterface */ private $pageRepository; @@ -50,21 +57,20 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac * @param Action\Context $context * @param PostDataProcessor $dataProcessor * @param DataPersistorInterface $dataPersistor - * @param \Magento\Cms\Model\PageFactory|null $pageFactory - * @param \Magento\Cms\Api\PageRepositoryInterface|null $pageRepository + * @param PageFactory|null $pageFactory + * @param PageRepositoryInterface|null $pageRepository */ public function __construct( Action\Context $context, PostDataProcessor $dataProcessor, DataPersistorInterface $dataPersistor, - \Magento\Cms\Model\PageFactory $pageFactory = null, - \Magento\Cms\Api\PageRepositoryInterface $pageRepository = null + PageFactory $pageFactory = null, + PageRepositoryInterface $pageRepository = null ) { $this->dataProcessor = $dataProcessor; $this->dataPersistor = $dataPersistor; - $this->pageFactory = $pageFactory ?: ObjectManager::getInstance()->get(\Magento\Cms\Model\PageFactory::class); - $this->pageRepository = $pageRepository - ?: ObjectManager::getInstance()->get(\Magento\Cms\Api\PageRepositoryInterface::class); + $this->pageFactory = $pageFactory ?: ObjectManager::getInstance()->get(PageFactory::class); + $this->pageRepository = $pageRepository ?: ObjectManager::getInstance()->get(PageRepositoryInterface::class); parent::__construct($context); } @@ -72,12 +78,12 @@ public function __construct( * Save action * * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface */ public function execute() { $data = $this->getRequest()->getPostValue(); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); if ($data) { $data = $this->dataProcessor->filter($data); @@ -88,7 +94,7 @@ public function execute() $data['page_id'] = null; } - /** @var \Magento\Cms\Model\Page $model */ + /** @var Page $model */ $model = $this->pageFactory->create(); $id = $this->getRequest()->getParam('page_id'); @@ -117,7 +123,7 @@ public function execute() } catch (LocalizedException $e) { $this->messageManager->addExceptionMessage($e->getPrevious() ?: $e); } catch (\Throwable $e) { - $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the page.')); + $this->messageManager->addErrorMessage(__('Something went wrong while saving the page.')); } $this->dataPersistor->set('cms_page', $data); @@ -129,10 +135,10 @@ public function execute() /** * Process result redirect * - * @param \Magento\Cms\Api\Data\PageInterface $model - * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect + * @param PageInterface $model + * @param Redirect $resultRedirect * @param array $data - * @return \Magento\Backend\Model\View\Result\Redirect + * @return Redirect * @throws LocalizedException */ private function processResultRedirect($model, $resultRedirect, $data) @@ -149,7 +155,7 @@ private function processResultRedirect($model, $resultRedirect, $data) '*/*/edit', [ 'page_id' => $newPage->getId(), - '_current' => true + '_current' => true, ] ); } diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php index 67a7f0a934480..1cd85364b2cdd 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php @@ -1,4 +1,5 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -7,6 +8,7 @@ namespace Magento\Cms\Test\Unit\Controller\Adminhtml\Page; +use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Redirect; use Magento\Backend\Model\View\Result\RedirectFactory; use Magento\Cms\Api\PageRepositoryInterface; @@ -18,7 +20,6 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -82,13 +83,14 @@ class SaveTest extends TestCase */ private $pageId = 1; + /** + * @inheirtDoc + */ protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) + ->onlyMethods(['create']) ->getMock(); $this->resultRedirect = $this->getMockBuilder(Redirect::class) ->disableOriginalConstructor() @@ -98,7 +100,7 @@ protected function setUp(): void ->willReturn($this->resultRedirect); $this->dataProcessorMock = $this->getMockBuilder( PostDataProcessor::class - )->setMethods(['filter'])->disableOriginalConstructor() + )->onlyMethods(['filter'])->disableOriginalConstructor() ->getMock(); $this->dataPersistorMock = $this->getMockBuilder(DataPersistorInterface::class) ->getMock(); @@ -108,27 +110,28 @@ protected function setUp(): void $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->setMethods(['dispatch']) + ->onlyMethods(['dispatch']) ->getMockForAbstractClass(); $this->pageFactory = $this->getMockBuilder(PageFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) + ->onlyMethods(['create']) ->getMock(); $this->pageRepository = $this->getMockBuilder(PageRepositoryInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->saveController = $objectManager->getObject( - Save::class, - [ - 'request' => $this->requestMock, - 'messageManager' => $this->messageManagerMock, - 'eventManager' => $this->eventManagerMock, - 'resultRedirectFactory' => $this->resultRedirectFactory, - 'dataProcessor' => $this->dataProcessorMock, - 'dataPersistor' => $this->dataPersistorMock, - 'pageFactory' => $this->pageFactory, - 'pageRepository' => $this->pageRepository - ] + $context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $context->method('getRequest')->willReturn($this->requestMock); + $context->method('getMessageManager')->willReturn($this->messageManagerMock); + $context->method('getEventManager')->willReturn($this->eventManagerMock); + $context->method('getResultRedirectFactory')->willReturn($this->resultRedirectFactory); + $this->saveController = new Save( + $context, + $this->dataProcessorMock, + $this->dataPersistorMock, + $this->pageFactory, + $this->pageRepository ); } @@ -140,7 +143,7 @@ public function testSaveAction() 'stores' => ['0'], 'is_active' => true, 'content' => '"><script>alert("cookie: "+document.cookie)</script>', - 'back' => 'close' + 'back' => 'close', ]; $filteredPostData = [ @@ -149,7 +152,7 @@ public function testSaveAction() 'stores' => ['0'], 'is_active' => true, 'content' => '"><script>alert("cookie: "+document.cookie)</script>', - 'back' => 'close' + 'back' => 'close', ]; $this->dataProcessorMock->expects($this->any()) @@ -236,7 +239,7 @@ public function testSaveAndContinue() 'stores' => ['0'], 'is_active' => true, 'content' => '"><script>alert("cookie: "+document.cookie)</script>', - 'back' => 'continue' + 'back' => 'continue', ]; $this->requestMock->expects($this->any())->method('getPostValue')->willReturn($postData); $this->requestMock->expects($this->atLeastOnce()) @@ -304,12 +307,13 @@ public function testSaveActionThrowsException() $this->pageRepository->expects($this->once())->method('getById')->with($this->pageId)->willReturn($page); $page->expects($this->once())->method('setData'); $this->pageRepository->expects($this->once())->method('save')->with($page) - ->willThrowException(new \Exception('Error message.')); + ->willThrowException(new \Error('Error message.')); $this->messageManagerMock->expects($this->never()) ->method('addSuccessMessage'); $this->messageManagerMock->expects($this->once()) - ->method('addExceptionMessage'); + ->method('addErrorMessage') + ->with('Something went wrong while saving the page.'); $this->dataPersistorMock->expects($this->any()) ->method('set') @@ -318,7 +322,7 @@ public function testSaveActionThrowsException() [ 'page_id' => $this->pageId, 'layout_update_xml' => null, - 'custom_layout_update_xml' => null + 'custom_layout_update_xml' => null, ] ); From 50a16d0513abb25f24e4493f010494bc24cbdd9b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 4 Mar 2021 14:38:22 +0200 Subject: [PATCH 098/137] MC-40679: A "Products" page of the Admin panel is constantly loading after sorting by FPT value was applied --- .../Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php b/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php index 7889d214b7dd4..31746a7e381c0 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php +++ b/dev/tests/integration/testsuite/Magento/Weee/Plugin/Catalog/Ui/Component/Listing/ColumnsTest.php @@ -15,7 +15,8 @@ use PHPUnit\Framework\TestCase; /** - * @magentoDbIsolation enabled + * Class ColumnsTest + * Check if FPT attribute column in product grid won't be sortable */ class ColumnsTest extends TestCase { @@ -48,8 +49,7 @@ protected function setUp(): void } /** - * Check if FPT attribute column in product grid won't be sortable - * + * @magentoDbIsolation enabled * @magentoDataFixture Magento/Weee/_files/fixed_product_attribute.php */ public function testGetProductWeeeAttributesConfig() From 4b36743829bbb626800f78cf751d3c7434697172 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 2 Mar 2021 15:49:20 -0600 Subject: [PATCH 099/137] MC-41030: FPT Doesn't show in GraphQL when the state is specified - Add new field fixed_product_taxes in CartItemPrices to return applied FPTs to the cart item --- .../Model/Resolver/CartItemPrices.php | 1 + .../Model/Resolver/Quote/FixedProductTax.php | 84 ++++ .../Resolver/FixedProductTaxResolverTest.php | 284 +++++++++++++ .../Magento/WeeeGraphQl/etc/schema.graphqls | 4 + .../Weee/CartItemPricesWithFPTTest.php | 374 ++++++++++++++++++ .../Weee/_files/add_fpt_for_region_1.php | 33 ++ .../add_simple_product_with_fpt_to_cart.php | 28 ++ .../apply_tax_for_simple_product_with_fpt.php | 26 ++ 8 files changed, 834 insertions(+) create mode 100644 app/code/Magento/WeeeGraphQl/Model/Resolver/Quote/FixedProductTax.php create mode 100644 app/code/Magento/WeeeGraphQl/Test/Unit/Model/Resolver/FixedProductTaxResolverTest.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php create mode 100644 dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_fpt_for_region_1.php create mode 100644 dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_simple_product_with_fpt_to_cart.php create mode 100644 dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/apply_tax_for_simple_product_with_fpt.php diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index d4ced5b8b97b0..d07a7241c9dd7 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -58,6 +58,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode(); return [ + 'model' => $cartItem, 'price' => [ 'currency' => $currencyCode, 'value' => $cartItem->getCalculationPrice(), diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/Quote/FixedProductTax.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/Quote/FixedProductTax.php new file mode 100644 index 0000000000000..3200887bf595d --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/Quote/FixedProductTax.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Model\Resolver\Quote; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Tax\Model\Config; +use Magento\Weee\Helper\Data; + +/** + * Resolver for FixedProductTax object that retrieves an array of FPT applied to a cart item + */ +class FixedProductTax implements ResolverInterface +{ + /** + * @var Data + */ + private $weeeHelper; + + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @param Data $weeeHelper + * @param TaxHelper $taxHelper + */ + public function __construct(Data $weeeHelper, TaxHelper $taxHelper) + { + $this->weeeHelper = $weeeHelper; + $this->taxHelper = $taxHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $fptArray = []; + $cartItem = $value['model']; + + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + if ($this->weeeHelper->isEnabled($store)) { + $taxes = $this->weeeHelper->getApplied($cartItem); + $displayInclTaxes = $this->taxHelper->getPriceDisplayType($store); + foreach ($taxes as $tax) { + $amount = $tax['amount']; + if ($displayInclTaxes === Config::DISPLAY_TYPE_INCLUDING_TAX) { + $amount = $tax['amount_incl_tax']; + } + $fptArray[] = [ + 'amount' => [ + 'value' => $amount, + 'currency' => $value['price']['currency'], + ], + 'label' => $tax['title'] + ]; + } + } + + return $fptArray; + } +} diff --git a/app/code/Magento/WeeeGraphQl/Test/Unit/Model/Resolver/FixedProductTaxResolverTest.php b/app/code/Magento/WeeeGraphQl/Test/Unit/Model/Resolver/FixedProductTaxResolverTest.php new file mode 100644 index 0000000000000..15edec0dd6145 --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Test/Unit/Model/Resolver/FixedProductTaxResolverTest.php @@ -0,0 +1,284 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Test\Unit\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextExtensionInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Weee\Helper\Data as WeeeHelper; +use Magento\WeeeGraphQl\Model\Resolver\Quote\FixedProductTax; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test FPT resolver for cart item + */ +class FixedProductTaxResolverTest extends TestCase +{ + /** + * @var MockObject|ContextInterface + */ + private $context; + + /** + * @var MockObject|WeeeHelper + */ + private $weeeHelper; + + /** + * @var TaxHelper|MockObject + */ + private $taxHelper; + + /** + * @var FixedProductTax + */ + private $resolver; + + /** + * @var array[] + */ + private $fpts = [ + [ + "title" => "FPT 2", + "base_amount" => "0.5000", + "amount" => 0.5, + "row_amount" => 1.0, + "base_row_amount" => 1.0, + "base_amount_incl_tax" => "0.5500", + "amount_incl_tax" => 0.55, + "row_amount_incl_tax" => 1.1, + "base_row_amount_incl_tax" => 1.1 + ], + [ + "title" => "FPT 1", + "base_amount" => "1.0000", + "amount" => 1, + "row_amount" => 2, + "base_row_amount" => 2, + "base_amount_incl_tax" => "1.1000", + "amount_incl_tax" => 1.1, + "row_amount_incl_tax" => 2.2, + "base_row_amount_incl_tax" => 2.2 + ], + [ + "title" => "FPT 2", + "base_amount" => "1.5000", + "amount" => 1.5, + "row_amount" => 3.0, + "base_row_amount" => 3.0, + "base_amount_incl_tax" => "1.6500", + "amount_incl_tax" => 1.65, + "row_amount_incl_tax" => 3.30, + "base_row_amount_incl_tax" => 3.30 + ] + ]; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->context = $this->getMockBuilder(ContextInterface::class) + ->setMethods(['getExtensionAttributes']) + ->getMockForAbstractClass(); + + $this->weeeHelper = $this->getMockBuilder(WeeeHelper::class) + ->disableOriginalConstructor() + ->onlyMethods(['isEnabled', 'getApplied']) + ->getMock(); + $this->taxHelper = $this->getMockBuilder(TaxHelper::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPriceDisplayType']) + ->getMock(); + + $this->resolver = new FixedProductTax( + $this->weeeHelper, + $this->taxHelper, + ); + } + + /** + * Verifies that exception is thrown if model is not specified + */ + public function testShouldThrowException(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/value should be specified/'); + + $this->resolver->resolve( + $this->getFieldStub(), + null, + $this->getResolveInfoStub() + ); + } + + /** + * Verifies that result is empty if FPT config is disabled + */ + public function testShouldReturnEmptyResult(): void + { + $store = $this->createMock(StoreInterface::class); + $cartItem = $this->createMock(CartItemInterface::class); + $contextExtensionAttributes = $this->createMock(ContextExtensionInterface::class); + $contextExtensionAttributes->method('getStore') + ->willreturn($store); + $this->context->method('getExtensionAttributes') + ->willReturn($contextExtensionAttributes); + + $this->weeeHelper->method('isEnabled') + ->with($store) + ->willReturn(false); + + $this->weeeHelper->expects($this->never()) + ->method('getApplied'); + + $this->assertEquals( + [], + $this->resolver->resolve( + $this->getFieldStub(), + $this->context, + $this->getResolveInfoStub(), + ['model' => $cartItem] + ) + ); + } + + /** + * @dataProvider shouldReturnResultDataProvider + * @param int $displayType + * @param array $expected + */ + public function testShouldReturnResult(int $displayType, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $cartItem = $this->createMock(CartItemInterface::class); + $contextExtensionAttributes = $this->createMock(ContextExtensionInterface::class); + $contextExtensionAttributes->method('getStore') + ->willreturn($store); + $this->context->method('getExtensionAttributes') + ->willReturn($contextExtensionAttributes); + + $this->weeeHelper->method('isEnabled') + ->with($store) + ->willReturn(true); + + $this->weeeHelper->expects($this->once()) + ->method('getApplied') + ->willReturn($this->fpts); + + $this->taxHelper->expects($this->once()) + ->method('getPriceDisplayType') + ->willReturn($displayType); + + $this->assertEquals( + $expected, + $this->resolver->resolve( + $this->getFieldStub(), + $this->context, + $this->getResolveInfoStub(), + [ + 'model' => $cartItem, + 'price' => [ + 'currency' => 'USD' + ] + ] + ) + ); + } + + /** + * @return array + */ + public function shouldReturnResultDataProvider(): array + { + return [ + [ + 1, + [ + [ + 'label' => 'FPT 2', + 'amount' => [ + 'value' => 0.5, + 'currency' => 'USD' + ] + ], + [ + 'label' => 'FPT 1', + 'amount' => [ + 'value' => 1, + 'currency' => 'USD' + ] + ], + [ + 'label' => 'FPT 2', + 'amount' => [ + 'value' => 1.5, + 'currency' => 'USD' + ] + ] + ] + ], + [ + 2, + [ + [ + 'label' => 'FPT 2', + 'amount' => [ + 'value' => 0.55, + 'currency' => 'USD' + ] + ], + [ + 'label' => 'FPT 1', + 'amount' => [ + 'value' => 1.1, + 'currency' => 'USD' + ] + ], + [ + 'label' => 'FPT 2', + 'amount' => [ + 'value' => 1.65, + 'currency' => 'USD' + ] + ] + ] + ] + ]; + } + + /** + * @return MockObject|Field + */ + private function getFieldStub(): Field + { + /** @var MockObject|Field $fieldMock */ + $fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + return $fieldMock; + } + + /** + * @return MockObject|ResolveInfo + */ + private function getResolveInfoStub(): ResolveInfo + { + /** @var MockObject|ResolveInfo $resolveInfoMock */ + $resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + return $resolveInfoMock; + } +} diff --git a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls index 18b0e7c1823e8..6d212f25618e2 100644 --- a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls @@ -10,6 +10,10 @@ type ProductPrice { fixed_product_taxes: [FixedProductTax] @doc(description: "The multiple FPTs that can be applied to a product price.") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\FixedProductTax") } +type CartItemPrices { + fixed_product_taxes: [FixedProductTax] @doc(description: "Applied FPT to the cart item.") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\Quote\\FixedProductTax") +} + type FixedProductTax @doc(description: "A single FPT that can be applied to a product price.") { amount: Money @doc(description: "Amount of the FPT as a money object.") label: String @doc(description: "The label assigned to the FPT to be displayed on the frontend.") diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php new file mode 100644 index 0000000000000..1d53bd03be3d4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php @@ -0,0 +1,374 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Weee; + +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for cart item fixed product tax + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + */ +class CartItemPricesWithFPTTest extends GraphQlAbstract +{ + /** + * @var ObjectManager $objectManager + */ + private $objectManager; + + /** + * @var string[] + */ + private $initialConfig; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + + $currentSettingsArray = [ + 'tax/display/type', + 'tax/weee/enable', + 'tax/weee/display', + 'tax/defaults/region', + 'tax/weee/apply_vat', + 'tax/calculation/price_includes_tax' + ]; + + foreach ($currentSettingsArray as $configPath) { + $this->initialConfig[$configPath] = $this->scopeConfig->getValue( + $configPath + ); + } + /** @var ReinitableConfigInterface $config */ + $config = $this->objectManager->get(ReinitableConfigInterface::class); + $config->reinit(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->writeConfig($this->initialConfig); + } + + /** + * Write configuration for weee + * + * @param array $settings + * @return void + */ + private function writeConfig(array $settings): void + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($settings as $path => $value) { + $configWriter->save($path, $value); + } + $this->scopeConfig->clean(); + } + + /** + * @param array $taxSettings + * @param array $expectedFtps + * @return void + * + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @dataProvider cartItemFixedProductTaxDataProvider + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/Weee/_files/product_with_two_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Weee/_files/add_fpt_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Weee/_files/apply_tax_for_simple_product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Weee/_files/add_simple_product_with_fpt_to_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testCartItemFixedProductTax(array $taxSettings, array $expectedFtps): void + { + $this->writeConfig($taxSettings); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['cart']['items']); + $actualFtps = $result['cart']['items'][0]['prices']['fixed_product_taxes']; + $this->assertEqualsCanonicalizing($expectedFtps, $actualFtps); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function cartItemFixedProductTaxDataProvider(): array + { + return [ + [ + 'taxSettings' => [ + 'tax/weee/enable' => '1', + 'tax/weee/apply_vat' => '0', + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '1', + ], + 'expectedFtps' => [ + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 10.0 + ] + ], + [ + 'label' => 'fpt_for_all_front_label', + 'amount' => [ + 'value' => 12.7 + ] + ], + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 5.0 + ] + ], + ] + ], + [ + 'taxSettings' => [ + 'tax/weee/enable' => '1', + 'tax/weee/apply_vat' => '0', + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '1', + ], + 'expectedFtps' => [ + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 10.0 + ] + ], + [ + 'label' => 'fpt_for_all_front_label', + 'amount' => [ + 'value' => 12.7 + ] + ], + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 5.0 + ] + ], + ] + ], + [ + 'taxSettings' => [ + 'tax/weee/enable' => '1', + 'tax/weee/apply_vat' => '1', + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '1', + ], + 'expectedFtps' => [ + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 10.0 + ] + ], + [ + 'label' => 'fpt_for_all_front_label', + 'amount' => [ + 'value' => 12.7 + ] + ], + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 5.0 + ] + ], + ] + ], + [ + 'taxSettings' => [ + 'tax/weee/enable' => '1', + 'tax/weee/apply_vat' => '1', + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '2', + ], + 'expectedFtps' => [ + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 10.75 + ] + ], + [ + 'label' => 'fpt_for_all_front_label', + 'amount' => [ + 'value' => 13.66 + ] + ], + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 5.38 + ] + ], + ] + ], + [ + 'taxSettings' => [ + 'tax/weee/enable' => '1', + 'tax/weee/apply_vat' => '1', + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '1', + ], + 'expectedFtps' => [ + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 10.0 + ] + ], + [ + 'label' => 'fpt_for_all_front_label', + 'amount' => [ + 'value' => 12.7 + ] + ], + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 5.01 + ] + ], + ] + ], + [ + 'taxSettings' => [ + 'tax/weee/enable' => '1', + 'tax/weee/apply_vat' => '1', + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + ], + 'expectedFtps' => [ + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 10.75 + ] + ], + [ + 'label' => 'fpt_for_all_front_label', + 'amount' => [ + 'value' => 13.65 + ] + ], + [ + 'label' => 'fixed_product_attribute_front_label', + 'amount' => [ + 'value' => 5.38 + ] + ], + ] + ], + [ + 'taxSettings' => [ + 'tax/weee/enable' => '0', + 'tax/weee/apply_vat' => '1', + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '1', + ], + 'expectedFtps' => [] + ] + ]; + } + + /** + * Generates GraphQl query for retrieving cart totals + * + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + items { + prices { + price { + value + currency + } + row_total { + value + currency + } + row_total_including_tax { + value + currency + } + fixed_product_taxes { + label + amount { + value + } + } + } + } + prices { + grand_total { + value + currency + } + subtotal_including_tax { + value + currency + } + subtotal_excluding_tax { + value + currency + } + subtotal_with_discount_excluding_tax { + value + currency + } + applied_taxes { + label + amount { + value + currency + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_fpt_for_region_1.php b/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_fpt_for_region_1.php new file mode 100644 index 0000000000000..3492d2464b7b9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_fpt_for_region_1.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Quote\Model\QuoteFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var $product Product */ +$product = $productRepository->get('simple-with-ftp', true); +if ($product && $product->getId()) { + $product->setFixedProductAttribute( + array_merge( + $product->getFixedProductAttribute() ?? [], + [ + [ + 'website_id' => 0, + 'country' => 'US', + 'state' => 1, + 'price' => 5.00, + 'delete' => '' + ] + ] + ) + ); + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_simple_product_with_fpt_to_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_simple_product_with_fpt_to_cart.php new file mode 100644 index 0000000000000..ba31fec026d3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/add_simple_product_with_fpt_to_cart.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('simple-with-ftp'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/apply_tax_for_simple_product_with_fpt.php b/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/apply_tax_for_simple_product_with_fpt.php new file mode 100644 index 0000000000000..fa10c89decf60 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Weee/_files/apply_tax_for_simple_product_with_fpt.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple-with-ftp'); + +/** @var TaxClassCollectionFactory $taxClassCollectionFactory */ +$taxClassCollectionFactory = $objectManager->get(TaxClassCollectionFactory::class); +$taxClassCollection = $taxClassCollectionFactory->create(); + +/** @var TaxClassModel $taxClass */ +$taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); +$taxClass = $taxClassCollection->getFirstItem(); + +$product->setCustomAttribute('tax_class_id', $taxClass->getClassId()); +$productRepository->save($product); From 850313d87cbd7de65419dc1b6d44790aa1809caf Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 4 Mar 2021 09:28:28 -0600 Subject: [PATCH 100/137] PWA-1326: Implement the schema changes for Configurable Options Selection - static --- app/code/Magento/SwatchesGraphQl/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 5b50a10abfe2b..959f0f201d2b3 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -8,7 +8,6 @@ "magento/module-swatches": "*", "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*" - }, "suggest": { "magento/module-configurable-product-graph-ql": "*" From d7c68b3d1333d53b527fada920da616e4cfbd273 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Thu, 4 Mar 2021 12:57:18 -0600 Subject: [PATCH 101/137] MC-40825: Product images not removed when deleting products - Delete product images via afterDelete method --- .../Adminhtml/Product/MassDelete.php | 18 +- .../MediaImageDeleteProcessor.php | 146 ++++++++++++ .../Catalog/Model/ResourceModel/Product.php | 23 +- .../MediaImageDeleteProcessorTest.php | 200 ++++++++++++++++ .../Adminhtml/Product/MassDeleteTest.php | 53 +++++ .../_files/product_simple_with_media.php | 222 ++++++++++++++++++ .../product_simple_with_media_rollback.php | 30 +++ .../_files/product_with_media_gallery.php | 35 +++ .../product_with_media_gallery_rollback.php | 11 + 9 files changed, 731 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery_rollback.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php index c779c01cd7d71..0edd439230600 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php @@ -9,9 +9,12 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Redirect; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Ui\Component\MassAction\Filter; @@ -20,7 +23,7 @@ /** * Class \Magento\Catalog\Controller\Adminhtml\Product\MassDelete */ -class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface +class MassDelete extends Product implements HttpPostActionInterface { /** * Massactions filter @@ -49,8 +52,8 @@ class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product implement * @param Builder $productBuilder * @param Filter $filter * @param CollectionFactory $collectionFactory - * @param ProductRepositoryInterface $productRepository - * @param LoggerInterface $logger + * @param ProductRepositoryInterface|null $productRepository + * @param LoggerInterface|null $logger */ public function __construct( Context $context, @@ -63,20 +66,23 @@ public function __construct( $this->filter = $filter; $this->collectionFactory = $collectionFactory; $this->productRepository = $productRepository ?: - \Magento\Framework\App\ObjectManager::getInstance()->create(ProductRepositoryInterface::class); + ObjectManager::getInstance()->create(ProductRepositoryInterface::class); $this->logger = $logger ?: - \Magento\Framework\App\ObjectManager::getInstance()->create(LoggerInterface::class); + ObjectManager::getInstance()->create(LoggerInterface::class); parent::__construct($context, $productBuilder); } /** * Mass Delete Action * - * @return \Magento\Backend\Model\View\Result\Redirect + * @return Redirect + * @throws LocalizedException */ public function execute() { $collection = $this->filter->getCollection($this->collectionFactory->create()); + $collection->addMediaGalleryData(); + $productDeleted = 0; $productDeletedError = 0; /** @var \Magento\Catalog\Model\Product $product */ diff --git a/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php new file mode 100644 index 0000000000000..f49ddef01ca74 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Gallery\Processor; +use Magento\Catalog\Model\Product\Media\ConfigInterface as MediaConfig; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Psr\Log\LoggerInterface; + +/** + * Process media gallery and delete media image after product delete + */ +class MediaImageDeleteProcessor +{ + /** + * @var MediaConfig + */ + private $imageConfig; + + /** + * @var Filesystem + */ + private $mediaDirectory; + + /** + * @var Processor + */ + private $imageProcessor; + + /** + * @var Gallery + */ + private $productGallery; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Product constructor. + * + * @param MediaConfig $imageConfig + * @param Filesystem $filesystem + * @param Processor $imageProcessor + * @param Gallery $productGallery + * @param LoggerInterface $logger + * @throws FileSystemException + */ + public function __construct( + MediaConfig $imageConfig, + Filesystem $filesystem, + Processor $imageProcessor, + Gallery $productGallery, + LoggerInterface $logger + ) { + $this->imageConfig = $imageConfig; + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->imageProcessor = $imageProcessor; + $this->productGallery = $productGallery; + $this->logger = $logger; + } + + /** + * Process $product data and remove image from gallery after product delete + * + * @param DataObject $product + * @return void + */ + public function execute(DataObject $product): void + { + try { + $productImages = $product->getMediaGalleryImages(); + foreach ($productImages as $image) { + $imageFile = $image->getFile(); + if ($imageFile) { + $this->deleteProductImage($image, $product, $imageFile); + } + } + } catch (Exception $e) { + $this->logger->critical($e); + } + } + + /** + * Check if image exists and is not used by any other products + * + * @param string $file + * @return bool + */ + private function canDeleteImage(string $file): bool + { + return $this->productGallery->countImageUses($file) <= 1; + } + + /** + * Delete the physical image if it's existed and not used by other products + * + * @param string $imageFile + * @param string $filePath + * @throws FileSystemException + */ + private function deletePhysicalImage(string $imageFile, string $filePath): void + { + if ($this->canDeleteImage($imageFile)) { + $this->mediaDirectory->delete($filePath); + } + } + + /** + * Remove product image + * + * @param DataObject $image + * @param ProductInterface $product + * @param string $imageFile + */ + private function deleteProductImage( + DataObject $image, + ProductInterface $product, + string $imageFile + ): void { + $catalogPath = $this->imageConfig->getBaseMediaPath(); + $filePath = $catalogPath . $imageFile; + + if ($this->mediaDirectory->isFile($filePath)) { + try { + $this->productGallery->deleteGallery($image->getValueId()); + $this->imageProcessor->removeImage($product, $imageFile); + $this->deletePhysicalImage($imageFile, $filePath); + } catch (Exception $e) { + $this->logger->critical($e); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index b174e4beb6353..b3c50015d9dbf 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -100,6 +100,11 @@ class Product extends AbstractResource */ private $eavAttributeManagement; + /** + * @var MediaImageDeleteProcessor + */ + private $mediaImageDeleteProcessor; + /** * @param \Magento\Eav\Model\Entity\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -114,6 +119,7 @@ class Product extends AbstractResource * @param TableMaintainer|null $tableMaintainer * @param UniqueValidationInterface|null $uniqueValidator * @param AttributeManagementInterface|null $eavAttributeManagement + * @param MediaImageDeleteProcessor|null $mediaImageDeleteProcessor * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -129,7 +135,8 @@ public function __construct( $data = [], TableMaintainer $tableMaintainer = null, UniqueValidationInterface $uniqueValidator = null, - AttributeManagementInterface $eavAttributeManagement = null + AttributeManagementInterface $eavAttributeManagement = null, + ?MediaImageDeleteProcessor $mediaImageDeleteProcessor = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -148,6 +155,8 @@ public function __construct( $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); $this->eavAttributeManagement = $eavAttributeManagement ?? ObjectManager::getInstance()->get(AttributeManagementInterface::class); + $this->mediaImageDeleteProcessor = $mediaImageDeleteProcessor + ?? ObjectManager::getInstance()->get(MediaImageDeleteProcessor::class); } /** @@ -819,4 +828,16 @@ protected function getAttributeRow($entity, $object, $attribute) $data['store_id'] = $object->getStoreId(); return $data; } + + /** + * After delete entity process + * + * @param DataObject $object + * @return $this + */ + protected function _afterDelete(DataObject $object) + { + $this->mediaImageDeleteProcessor->execute($object); + return parent::_afterDelete($object); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php new file mode 100644 index 0000000000000..bfc113ce41609 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php @@ -0,0 +1,200 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\MediaImageDeleteProcessor; +use Magento\Catalog\Model\Product\Gallery\Processor; +use Magento\Catalog\Model\Product\Media\ConfigInterface as MediaConfig; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Unit test for \Magento\Catalog\Model\ResourceModel\MediaImageDeleteProcessor + */ +class MediaImageDeleteProcessorTest extends TestCase +{ + /** + * Testable Object + * + * @var MediaImageDeleteProcessor + */ + private $mediaImageDeleteProcessor; + + /** + * @var ObjectManager|null + */ + private $objectManager; + + /** + * @var Product|MockObject + */ + private $productMock; + + /** + * @var MediaConfig|MockObject + */ + private $imageConfig; + + /** + * @var Filesystem|MockObject + */ + private $mediaDirectory; + + /** + * @var Processor|MockObject + */ + private $imageProcessor; + + /** + * @var Gallery|MockObject + */ + private $productGallery; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getMediaGalleryImages']) + ->getMock(); + + $this->imageConfig = $this->getMockBuilder(MediaConfig::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseMediaUrl', 'getMediaUrl', 'getBaseMediaPath', 'getMediaPath']) + ->getMock(); + + $this->mediaDirectory = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->setMethods(['getRelativePath', 'isFile', 'delete']) + ->getMock(); + + $this->imageProcessor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->setMethods(['removeImage']) + ->getMock(); + + $this->productGallery = $this->getMockBuilder(Gallery::class) + ->disableOriginalConstructor() + ->setMethods(['deleteGallery', 'countImageUses']) + ->getMock(); + + $this->mediaImageDeleteProcessor = $this->objectManager->getObject( + MediaImageDeleteProcessor::class, + [ + 'imageConfig' => $this->imageConfig, + 'mediaDirectory' => $this->mediaDirectory, + 'imageProcessor' => $this->imageProcessor, + 'productGallery' => $this->productGallery + ] + ); + } + + /** + * Test mediaImageDeleteProcessor execute method + * + * @dataProvider executeCategoryProductMediaDeleteDataProvider + * @param int $productId + * @param array $productImages + * @param bool $isValidFile + * @param bool $imageUsedBefore + */ + public function testExecuteCategoryProductMediaDelete( + int $productId, + array $productImages, + bool $isValidFile, + bool $imageUsedBefore + ): void { + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn($productId); + + $this->productMock->expects($this->any()) + ->method('getMediaGalleryImages') + ->willReturn($productImages); + + $this->mediaDirectory->expects($this->any()) + ->method('isFile') + ->willReturn($isValidFile); + + $this->mediaDirectory->expects($this->any()) + ->method('getRelativePath') + ->withConsecutive([$productImages[0]->getFile()], [$productImages[1]->getFile()]) + ->willReturnOnConsecutiveCalls($productImages[0]->getPath(), $productImages[1]->getPath()); + + $this->productGallery->expects($this->any()) + ->method('countImageUses') + ->willReturn($imageUsedBefore); + + $this->productGallery->expects($this->any()) + ->method('deleteGallery') + ->willReturnSelf(); + + $this->imageProcessor->expects($this->any()) + ->method('removeImage') + ->willReturnSelf(); + + $this->mediaImageDeleteProcessor->execute($this->productMock); + } + + /** + * @return array + */ + public function executeCategoryProductMediaDeleteDataProvider(): array + { + $imageDirectoryPath = '/media/dir1/dir2/catalog/product/'; + $image1FilePath = '/test/test1.jpg'; + $image2FilePath = '/test/test2.jpg'; + $productImages = [ + new DataObject([ + 'value_id' => 1, + 'file' => $image1FilePath, + 'media_type' => 'image', + 'path' => $imageDirectoryPath.$image1FilePath + ]), + new DataObject([ + 'value_id' => 2, + 'file' => $image2FilePath, + 'media_type' => 'image', + 'path' => $imageDirectoryPath.$image2FilePath + ]) + ]; + return [ + 'test image can be deleted with existing product and product images' => + [ + 12, + $productImages, + true, + false + ], + 'test image can not be deleted without valid product id' => + [ + 0, + $productImages, + true, + false + ], + 'test image can not be deleted without valid product images' => + [ + 12, + [new DataObject(['file' => null]), new DataObject(['file' => null])], + true, + false + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php index 6384883c56c58..421526a5f8297 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php @@ -7,10 +7,16 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\ObjectManagerInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; /** * Test for mass product deleting. @@ -21,9 +27,29 @@ */ class MassDeleteTest extends AbstractBackendController { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** @var ProductRepositoryInterface */ protected $productRepository; + /** + * @var ProductResource + */ + private $productResource; + + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var int + */ + private $mediaAttributeId; + /** * @inheritdoc */ @@ -31,7 +57,11 @@ protected function setUp(): void { parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->productResource = $this->objectManager->create(ProductResource::class); + $this->galleryResource = $this->objectManager->create(Gallery::class); + $this->mediaAttributeId = (int)$this->productResource->getAttribute('media_gallery')->getAttributeId(); $this->productRepository->cleanCache(); } @@ -47,6 +77,29 @@ public function testDeleteSimpleProductViaMassAction(): void $this->assertSuccessfulDeleteProducts(count($productIds)); } + /** + * Tests image remove during product delete. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_media_gallery.php + * + * @return void + * @throws NoSuchEntityException + */ + public function testDeleteSimpleProductWithImageViaMassAction(): void + { + $productIds = [812]; + $product = $this->productRepository->get( + 'simple_product_with_media', + false, + Store::DEFAULT_STORE_ID, + true + ); + $this->dispatchMassDeleteAction($productIds); + $this->assertSuccessfulDeleteProducts(count($productIds)); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); + $this->assertCount(0, $productImages); + } + /** * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media.php new file mode 100644 index 0000000000000..3542e372dc0dc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Customer\Model\Group; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); + +$tierPrices = []; +/** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +/** @var $tpExtensionAttributes */ +$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var $productExtensionAttributes */ +$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); + +$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class)->get('admin'); +$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()); +$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( + ['website_ids' => $adminWebsite->getId()] +); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'qty' => 2, + 'value' => 8 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'qty' => 5, + 'value' => 5 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 3, + 'value' => 5 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 3.2, + 'value' => 6, + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()) + ->setPercentageValue(50); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 10 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes2); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setId(812) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product With Media') + ->setSku('simple_product_with_media') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(2) + ->setTierPrices($tierPrices) + ->setDescription('Description with <b>html tag</b>') + ->setExtensionAttributes($productExtensionAttributesWebsiteIds) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +$oldOptions = [ + [ + 'previous_group' => 'text', + 'title' => 'Test Field', + 'type' => 'field', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 1, + 'price_type' => 'fixed', + 'sku' => '1-text', + 'max_characters' => 100, + ], + [ + 'previous_group' => 'date', + 'title' => 'Test Date and Time', + 'type' => 'date_time', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 2, + 'price_type' => 'fixed', + 'sku' => '2-date', + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Select', + 'type' => 'drop_down', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '3-1-select', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '3-2-select', + ], + ] + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Radio', + 'type' => 'radio', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '4-1-radio', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '4-2-radio', + ], + ] + ] +]; + +$options = []; + +/** @var ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->create(ProductCustomOptionInterfaceFactory::class); + +foreach ($oldOptions as $option) { + /** @var ProductCustomOptionInterface $option */ + $option = $customOptionFactory->create(['data' => $option]); + $option->setProductSku($product->getSku()); + + $options[] = $option; +} + +$product->setOptions($options); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media_rollback.php new file mode 100644 index 0000000000000..cd29cd031f31a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_media_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple_product_with_media', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php new file mode 100644 index 0000000000000..eef2a371ce685 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_image.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_with_media.php'); + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple_product_with_media'); + +/** @var $product Product */ +$product->setStoreId(0) + ->setImage('/m/a/magento_image.jpg') + ->setSmallImage('/m/a/magento_image.jpg') + ->setThumbnail('/m/a/magento_image.jpg') + ->setData('media_gallery', ['images' => [ + [ + 'file' => '/m/a/magento_image.jpg', + 'position' => 1, + 'label' => 'Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ], + ]]) + ->setCanSaveCustomOptions(true) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery_rollback.php new file mode 100644 index 0000000000000..9b941d7359d47 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_with_media_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_image_rollback.php'); From 2b8a00109a8f6276cd948033acfe40d3746f116e Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 5 Mar 2021 10:29:11 +0200 Subject: [PATCH 102/137] MC-41174: [MFTF] AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest failed because of bad design --- .../AdminClickOnAdvancedInventoryLinkActionGroup.xml | 2 ++ .../AdminCatalogInventoryChangeManageStockActionGroup.xml | 2 ++ .../AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml index 60438e23e084c..2fb18ddfc9eb6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml @@ -17,5 +17,7 @@ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <!-- Wait for close button appeared. That means animation finished and modal window is fully visible --> + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="waitForCloseButtonAppeared"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml index 2c38f14f53379..b93c2af43e64d 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml @@ -16,6 +16,8 @@ <argument name="manageStock" type="string" defaultValue="Yes"/> </arguments> <conditionalClick selector="{{AdminProductFormSection.advancedInventoryLink}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.advancedInventoryModal}}" visible="false" stepKey="openAdvancedInventoryWindow"/> + <!-- Wait for close button appeared. That means animation finished and modal window is fully visible --> + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="waitForCloseButtonAppeared"/> <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="waitForAdvancedInventoryModalWindowOpen"/> <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckManageStockConfigSetting"/> <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="{{manageStock}}" stepKey="changeManageStock"/> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml index a5e4d3e9c2af7..e8871365dabea 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml @@ -13,11 +13,11 @@ <argument name="qty" type="string"/> </arguments> <conditionalClick selector="{{AdminProductFormSection.advancedInventoryLink}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.advancedInventoryModal}}" visible="false" stepKey="clickOnAdvancedInventoryLinkIfNeeded"/> + <!-- Wait for close button appeared. That means animation finished and modal window is fully visible --> + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="waitForCloseButtonAppeared"/> <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="waitForAdvancedInventoryModalWindowOpen"/> <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="{{qty}}" stepKey="fillMaxAllowedQty"/> <click selector="{{AdminSlideOutDialogSection.doneButton}}" stepKey="clickDone"/> </actionGroup> - - </actionGroups> From ef7092916d7f75a6d002e860c7e4fcb6a829a7bf Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 5 Mar 2021 10:54:45 +0200 Subject: [PATCH 103/137] MC-40873: Product is shown as Out of Stock on CMS page when Category Permissions are enabled --- .../Magento/Catalog/Block/Ui/ProductViewCounter.php | 1 + .../view/base/web/js/product/addtocart-button.js | 10 ++++++++++ .../web/template/product/addtocart-button.html | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php index c5c08a0552f42..93732b1b52f0c 100644 --- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php +++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php @@ -153,6 +153,7 @@ public function getCurrentProductData() $this->productRenderCollectorComposite ->collect($product, $productRender); $data = $this->hydrator->extract($productRender); + $data['is_available'] = $product->isAvailable(); $currentProductData = [ 'items' => [ diff --git a/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js b/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js index 4baf082b37c02..f599d05ba5ea9 100644 --- a/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js +++ b/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js @@ -55,6 +55,16 @@ define([ return row['is_salable']; }, + /** + * Depends on this option, stock status text can be "In stock" or "Out Of Stock" + * + * @param {Object} row + * @returns {Boolean} + */ + isAvailable: function (row) { + return row['is_available']; + }, + /** * Depends on this option, "Add to cart" button can be shown or hide. Depends on backend configuration * diff --git a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html index 05dbf02703285..867b5f40d98db 100644 --- a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html +++ b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html @@ -15,10 +15,10 @@ </button> </if> - <ifnot args="isSalable($row())"> + <if args="isAvailable($row()) === false"> <div class="stock unavailable"> <text args="$t('Availability')"/> <span translate="'Out of stock'"/> </div> - </ifnot> + </if> </if> From 9449bd054100476b27a61d4e0d4ccc24a3db6ddb Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 5 Mar 2021 13:10:56 +0200 Subject: [PATCH 104/137] MC-23989: Mini cart missing when you edit inline welcome message for guest and use special characters --- .../Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml index f0beab9d1368c..b24917c7fe818 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml @@ -15,7 +15,7 @@ <title value="Inline translation with quote symbols"/> <description value="As merchant I want to be able to rename text labels using quote symbols in it"/> <severity value="CRITICAL"/> - <testCaseId value="MC-"/> + <testCaseId value="MC-41175"/> <useCaseId value="MC-23989"/> <group value="translation"/> <group value="developer_mode_only"/> From 05b4665ce56a3f231bfddd766a9a003134d23681 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 5 Mar 2021 13:14:26 +0200 Subject: [PATCH 105/137] MC-40873: Product is shown as Out of Stock on CMS page when Category Permissions are enabled --- .../Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php index 6026d1462e461..87f5be4b21333 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php @@ -166,6 +166,7 @@ public function testGetCurrentProductDataWithNonEmptyProduct() { $productMock = $this->getMockBuilder(ProductInterface::class) ->disableOriginalConstructor() + ->addMethods(['isAvailable']) ->getMockForAbstractClass(); $productRendererMock = $this->getMockBuilder(ProductRenderInterface::class) ->disableOriginalConstructor() @@ -173,7 +174,6 @@ public function testGetCurrentProductDataWithNonEmptyProduct() $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->getMock(); - $this->registryMock->expects($this->once()) ->method('registry') ->with('product') From 9d666ccbac83a9678a3c7d7d5bd80bd67c764cc2 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Fri, 5 Mar 2021 16:01:39 +0200 Subject: [PATCH 106/137] MC-41069: Free Shipping enables with wrong total for DHL --- .../Model/Carrier/AbstractCarrierOnline.php | 12 ++- .../Magento/Dhl/Model/CarrierTest.php | 102 ++++++++++++++++-- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index c2238ff1a3809..c2331b294769a 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -648,13 +648,23 @@ public function setRawRequest($request) */ public function getMethodPrice($cost, $method = '') { + if ($method == $this->getConfigData( + $this->_freeMethod + ) && $this->getConfigFlag( + 'free_shipping_enable' + ) && $this->getConfigData( + 'free_shipping_subtotal' + ) <= $this->_request->getBaseSubtotalWithDiscountInclTax()) { + return '0.00'; + } + return $method == $this->getConfigData( $this->_freeMethod ) && $this->getConfigFlag( 'free_shipping_enable' ) && $this->getConfigData( 'free_shipping_subtotal' - ) <= $this->_rawRequest->getBaseSubtotalInclTax() ? '0.00' : $this->getFinalPriceWithHandlingFee( + ) <= $this->_request->getBaseSubtotalWithDiscountInclTax() ? '0.00' : $this->getFinalPriceWithHandlingFee( $cost ); } diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index 552040489e253..af236b5d5212f 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -443,14 +443,7 @@ private function getExpectedLabelRequestXml( public function testCollectRates() { $requestData = $this->getRequestData(); - //phpcs:disable Magento2.Functions.DiscouragedFunction - $response = new Response( - 200, - [], - file_get_contents(__DIR__ . '/../_files/dhl_quote_response.xml') - ); - //phpcs:enable Magento2.Functions.DiscouragedFunction - $this->httpClient->nextResponses(array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response)); + $this->setNextResponse(__DIR__ . '/../_files/dhl_quote_response.xml'); /** @var RateRequest $request */ $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); $expectedRates = [ @@ -563,6 +556,80 @@ private function setDhlConfig(array $params) } } + /** + * Tests that the free rate is returned when sending a quotes request + * + * @param array $addRequestData + * @param bool $freeShippingExpects + * @magentoConfigFixture default_store carriers/dhl/active 1 + * @magentoConfigFixture default_store carriers/dhl/id some ID + * @magentoConfigFixture default_store carriers/dhl/shipment_days Mon,Tue,Wed,Thu,Fri,Sat + * @magentoConfigFixture default_store carriers/dhl/intl_shipment_days Mon,Tue,Wed,Thu,Fri,Sat + * @magentoConfigFixture default_store carriers/dhl/allowed_methods IE + * @magentoConfigFixture default_store carriers/dhl/international_service IE + * @magentoConfigFixture default_store carriers/dhl/gateway_url https://xmlpi-ea.dhl.com/XMLShippingServlet + * @magentoConfigFixture default_store carriers/dhl/id some ID + * @magentoConfigFixture default_store carriers/dhl/password some password + * @magentoConfigFixture default_store carriers/dhl/content_type N + * @magentoConfigFixture default_store carriers/dhl/nondoc_methods 1,3,4,8,P,Q,E,F,H,J,M,V,Y + * @magentoConfigFixture default_store carriers/dhl/showmethod' => 1, + * @magentoConfigFixture default_store carriers/dhl/title DHL Title + * @magentoConfigFixture default_store carriers/dhl/specificerrmsg dhl error message + * @magentoConfigFixture default_store carriers/dhl/unit_of_measure K + * @magentoConfigFixture default_store carriers/dhl/size 1 + * @magentoConfigFixture default_store carriers/dhl/height 1.6 + * @magentoConfigFixture default_store carriers/dhl/width 1.6 + * @magentoConfigFixture default_store carriers/dhl/depth 1.6 + * @magentoConfigFixture default_store carriers/dhl/debug 1 + * @magentoConfigFixture default_store carriers/dhl/free_method_nondoc P + * @magentoConfigFixture default_store carriers/dhl/free_shipping_enable 1 + * @magentoConfigFixture default_store carriers/dhl/free_shipping_subtotal 25 + * @magentoConfigFixture default_store shipping/origin/country_id GB + * @magentoAppIsolation enabled + * @dataProvider collectRatesWithFreeShippingDataProvider + */ + public function testCollectRatesWithFreeShipping(array $addRequestData, bool $freeShippingExpects): void + { + $this->setNextResponse(__DIR__ . '/../_files/dhl_quote_response.xml'); + + $requestData = $this->getRequestData(); + $requestData['data'] += $addRequestData; + /** @var RateRequest $request */ + $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + + $actualRates = $this->dhlCarrier->collectRates($request)->getAllRates(); + $freeRateExists = false; + foreach ($actualRates as $i => $actualRate) { + $actualRate = $actualRate->getData(); + if ($actualRate['method'] === 'P' && $actualRate['price'] === 0.0) { + $freeRateExists = true; + } + } + + self::assertEquals($freeShippingExpects, $freeRateExists); + } + + /** + * @return array + */ + public function collectRatesWithFreeShippingDataProvider(): array + { + return [ + [ + ['base_subtotal_incl_tax' => 25, 'base_subtotal_with_discount_incl_tax' => 22], + false + ], + [ + ['base_subtotal_incl_tax' => 25, 'base_subtotal_with_discount_incl_tax' => 25], + true + ], + [ + ['base_subtotal_incl_tax' => 28, 'base_subtotal_with_discount_incl_tax' => 25], + true + ], + ]; + } + /** * Returns request data. * @@ -614,4 +681,23 @@ private function getRequestData(): array ] ]; } + + /** + * Set next response content from file + * + * @param string $file + */ + private function setNextResponse(string $file): void + { + //phpcs:disable Magento2.Functions.DiscouragedFunction + $response = new Response( + 200, + [], + file_get_contents($file) + ); + //phpcs:enable Magento2.Functions.DiscouragedFunction + $this->httpClient->nextResponses( + array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response) + ); + } } From 01dd146c79c0d25884eaa18a8962717ac710f06e Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Fri, 5 Mar 2021 16:02:20 +0200 Subject: [PATCH 107/137] MC-41069: Free Shipping enables with wrong total for DHL --- .../Shipping/Model/Carrier/AbstractCarrierOnline.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index c2331b294769a..45ccf38a95ef0 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -648,16 +648,6 @@ public function setRawRequest($request) */ public function getMethodPrice($cost, $method = '') { - if ($method == $this->getConfigData( - $this->_freeMethod - ) && $this->getConfigFlag( - 'free_shipping_enable' - ) && $this->getConfigData( - 'free_shipping_subtotal' - ) <= $this->_request->getBaseSubtotalWithDiscountInclTax()) { - return '0.00'; - } - return $method == $this->getConfigData( $this->_freeMethod ) && $this->getConfigFlag( From e5b3bb3e9d379b22c643d5db3108c2a1df14aa81 Mon Sep 17 00:00:00 2001 From: mamsincl <robert.szeker@ayko.com> Date: Fri, 5 Mar 2021 17:33:02 +0000 Subject: [PATCH 108/137] Update ListProduct.php use `current()` method of ArrayIterator object instead of `current()` php function --- app/code/Magento/Catalog/Block/Product/ListProduct.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 76fcdfbf232e5..a5bac3a5b5483 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -458,7 +458,7 @@ private function initializeProductCollection() // if the product is associated with any category if ($categories->count()) { // show products from this category - $this->setCategoryId(current($categories->getIterator())->getId()); + $this->setCategoryId($categories->getIterator()->current()->getId()); } } From b13fcbb5e6f30d5952267ca64d2006b9c62a6cbc Mon Sep 17 00:00:00 2001 From: Robert Szeker <robert.szeker@ayko.com> Date: Sun, 7 Mar 2021 15:36:06 +0000 Subject: [PATCH 109/137] Update unit test of Block/Product/ListProduct modify return value of GetIteration on catCollectionMock --- .../Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php index d1b01db75927c..f3a9d0089be9a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php @@ -195,7 +195,7 @@ public function testGetIdentities() $this->catCollectionMock->expects($this->once()) ->method('getIterator') - ->willReturn([$currentCategory]); + ->willReturn(new \ArrayIterator([$currentCategory])); $this->prodCollectionMock->expects($this->any()) ->method('getIterator') From a57b22b86f51cf460030169c89fc592462dab89b Mon Sep 17 00:00:00 2001 From: Jimmy <jimmy@mageplaza.com> Date: Mon, 8 Mar 2021 17:24:08 +0700 Subject: [PATCH 110/137] fix in admin, at reorder page, `Click to change shipping method` does not show radio button to select --- .../templates/order/create/shipping/method/form.phtml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml index fd5b7a55b4960..d745a265e6d04 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml @@ -38,11 +38,6 @@ $taxHelper = $block->getData('taxHelper'); value="<?= $block->escapeHtmlAttr($_code) ?>" id="s_method_<?= $block->escapeHtmlAttr($_code) ?>" <?= /* @noEscape */ $_checked ?> class="admin__control-radio required-entry"/> - <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( - 'onclick', - "order.setShippingMethod(this.value)", - 'input#s_method_' . $block->escapeHtmlAttr($_code) - ) ?> <label class="admin__field-label" for="s_method_<?= $block->escapeHtmlAttr($_code) ?>"> <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - @@ -59,6 +54,11 @@ $taxHelper = $block->getData('taxHelper'); <?php endif; ?> </strong> </label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setShippingMethod(this.value)", + 'input#s_method_' . $block->escapeHtmlAttr($_code) + ) ?> <?php endif; ?> </li> <?php endforeach; ?> From bff0f37834f5ff091d891d467d6a573532d769b5 Mon Sep 17 00:00:00 2001 From: Jimmy <jimmy@mageplaza.com> Date: Tue, 9 Mar 2021 15:06:48 +0700 Subject: [PATCH 111/137] fix minify error --- .../adminhtml/web/js/validation/validate-image-description.js | 2 +- .../view/adminhtml/web/js/validation/validate-image-title.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js index 127f1676015f1..eec95764a7ef5 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js @@ -12,7 +12,7 @@ define([ $.validator.addMethod( 'validate-image-description', function (value) { - return /^[a-zA-Z0-9\-\_\.\,\n\ ]+$|^$/i.test(value); + return /^[a-zA-Z0-9\-\_\.\,\n\s]+$|^$/i.test(value); }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), ' + 'dots (.), commas(,), underscores (_), dashes (-), and spaces on this field.')); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js index 1429be64b7d12..0a55a22ab1a10 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js @@ -12,7 +12,7 @@ define([ $.validator.addMethod( 'validate-image-title', function (value) { - return /^[a-zA-Z0-9\-\_\.\,\ ]+$/i.test(value); + return /^[a-zA-Z0-9\-\_\.\,\s]+$/i.test(value); }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + 'underscores (_), dashes(-) and spaces on this field.')); From 33b7c95a5c62cd50bce136b0ae62f667ac77fd7e Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Tue, 9 Mar 2021 10:25:16 +0200 Subject: [PATCH 112/137] MC-41069: Free Shipping enables with wrong total for DHL --- .../Model/Carrier/AbstractCarrierOnline.php | 2 +- .../Magento/Dhl/Model/CarrierTest.php | 125 +++++++++--------- 2 files changed, 66 insertions(+), 61 deletions(-) diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index 45ccf38a95ef0..f88fecf84be6a 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -654,7 +654,7 @@ public function getMethodPrice($cost, $method = '') 'free_shipping_enable' ) && $this->getConfigData( 'free_shipping_subtotal' - ) <= $this->_request->getBaseSubtotalWithDiscountInclTax() ? '0.00' : $this->getFinalPriceWithHandlingFee( + ) <= $this->_rawRequest->getValueWithDiscount() ? '0.00' : $this->getFinalPriceWithHandlingFee( $cost ); } diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index af236b5d5212f..57c6042ca25c9 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -442,10 +442,8 @@ private function getExpectedLabelRequestXml( */ public function testCollectRates() { - $requestData = $this->getRequestData(); $this->setNextResponse(__DIR__ . '/../_files/dhl_quote_response.xml'); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $request = $this->createRequest(); $expectedRates = [ ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 45.85, 'method' => 'E', 'price' => 45.85], ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 35.26, 'method' => 'Q', 'price' => 35.26], @@ -480,11 +478,9 @@ public function testCollectRates() */ public function testCollectRatesWithoutDimensions(?string $size, ?string $height, ?string $width, ?string $depth) { - $requestData = $this->getRequestData(); $this->setDhlConfig(['size' => $size, 'height' => $height, 'width' => $width, 'depth' => $depth]); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $request = $this->createRequest(); $this->dhlCarrier = Bootstrap::getObjectManager()->create(Carrier::class); $this->dhlCarrier->collectRates($request)->getAllRates(); @@ -504,15 +500,13 @@ public function testCollectRatesWithoutDimensions(?string $size, ?string $height public function testGetRatesWithHttpException(): void { $this->setDhlConfig(['showmethod' => 1]); - $requestData = $this->getRequestData(); $deferredResponse = $this->getMockBuilder(HttpResponseDeferredInterface::class) ->onlyMethods(['get']) ->getMockForAbstractClass(); $exception = new HttpException('Exception message'); $deferredResponse->method('get')->willThrowException($exception); $this->httpClient->setDeferredResponseMock($deferredResponse); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $request = $this->createRequest(); $this->dhlCarrier = Bootstrap::getObjectManager()->create(Carrier::class); $resultRate = $this->dhlCarrier->collectRates($request)->getAllRates()[0]; $error = Bootstrap::getObjectManager()->get(Error::class); @@ -591,18 +585,15 @@ private function setDhlConfig(array $params) public function testCollectRatesWithFreeShipping(array $addRequestData, bool $freeShippingExpects): void { $this->setNextResponse(__DIR__ . '/../_files/dhl_quote_response.xml'); - - $requestData = $this->getRequestData(); - $requestData['data'] += $addRequestData; - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $request = $this->createRequest($addRequestData); $actualRates = $this->dhlCarrier->collectRates($request)->getAllRates(); $freeRateExists = false; - foreach ($actualRates as $i => $actualRate) { + foreach ($actualRates as $actualRate) { $actualRate = $actualRate->getData(); - if ($actualRate['method'] === 'P' && $actualRate['price'] === 0.0) { + if ($actualRate['method'] === 'P' && (float)$actualRate['price'] === 0.0) { $freeRateExists = true; + break; } } @@ -616,15 +607,15 @@ public function collectRatesWithFreeShippingDataProvider(): array { return [ [ - ['base_subtotal_incl_tax' => 25, 'base_subtotal_with_discount_incl_tax' => 22], + ['package_value' => 25, 'package_value_with_discount' => 22], false ], [ - ['base_subtotal_incl_tax' => 25, 'base_subtotal_with_discount_incl_tax' => 25], + ['package_value' => 25, 'package_value_with_discount' => 25], true ], [ - ['base_subtotal_incl_tax' => 28, 'base_subtotal_with_discount_incl_tax' => 25], + ['package_value' => 28, 'package_value_with_discount' => 25], true ], ]; @@ -638,47 +629,45 @@ public function collectRatesWithFreeShippingDataProvider(): array private function getRequestData(): array { return [ - 'data' => [ - 'dest_country_id' => 'DE', - 'dest_region_id' => '82', - 'dest_region_code' => 'BER', - 'dest_street' => 'Turmstraße 17', - 'dest_city' => 'Berlin', - 'dest_postcode' => '10559', - 'dest_postal' => '10559', - 'package_value' => '5', - 'package_value_with_discount' => '5', - 'package_weight' => '8.2657', - 'package_qty' => '1', - 'package_physical_value' => '5', - 'free_method_weight' => '5', - 'store_id' => '1', - 'website_id' => '1', - 'free_shipping' => '0', - 'limit_carrier' => null, - 'base_subtotal_incl_tax' => '5', - 'orig_country_id' => 'US', - 'orig_region_id' => '12', - 'orig_city' => 'Fremont', - 'orig_postcode' => '94538', - 'dhl_id' => 'MAGEN_8501', - 'dhl_password' => 'QR2GO1U74X', - 'dhl_account' => '799909537', - 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', - 'girth' => null, - 'height' => null, - 'length' => null, - 'width' => null, - 'weight' => 1, - 'dhl_shipment_type' => 'P', - 'dhl_duitable' => 0, - 'dhl_duty_payment_type' => 'R', - 'dhl_content_desc' => 'Big Box', - 'limit_method' => 'IE', - 'ship_date' => '2014-01-09', - 'action' => 'RateEstimate', - 'all_items' => [], - ] + 'dest_country_id' => 'DE', + 'dest_region_id' => '82', + 'dest_region_code' => 'BER', + 'dest_street' => 'Turmstraße 17', + 'dest_city' => 'Berlin', + 'dest_postcode' => '10559', + 'dest_postal' => '10559', + 'package_value' => '5', + 'package_value_with_discount' => '5', + 'package_weight' => '8.2657', + 'package_qty' => '1', + 'package_physical_value' => '5', + 'free_method_weight' => '5', + 'store_id' => '1', + 'website_id' => '1', + 'free_shipping' => '0', + 'limit_carrier' => null, + 'base_subtotal_incl_tax' => '5', + 'orig_country_id' => 'US', + 'orig_region_id' => '12', + 'orig_city' => 'Fremont', + 'orig_postcode' => '94538', + 'dhl_id' => 'MAGEN_8501', + 'dhl_password' => 'QR2GO1U74X', + 'dhl_account' => '799909537', + 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', + 'girth' => null, + 'height' => null, + 'length' => null, + 'width' => null, + 'weight' => 1, + 'dhl_shipment_type' => 'P', + 'dhl_duitable' => 0, + 'dhl_duty_payment_type' => 'R', + 'dhl_content_desc' => 'Big Box', + 'limit_method' => 'IE', + 'ship_date' => '2014-01-09', + 'action' => 'RateEstimate', + 'all_items' => [], ]; } @@ -700,4 +689,20 @@ private function setNextResponse(string $file): void array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response) ); } + + /** + * Create Rate Request + * + * @param array $addRequestData + * @return RateRequest + */ + private function createRequest(array $addRequestData = []): RateRequest + { + $requestData = $this->getRequestData(); + if (!empty($addRequestData)) { + $requestData = array_merge($requestData, $addRequestData); + } + + return Bootstrap::getObjectManager()->create(RateRequest::class, ['data' => $requestData]); + } } From e9f7a3cf86fb9e55272514be2de3f2a20d6f2570 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Tue, 9 Mar 2021 02:44:11 -0600 Subject: [PATCH 113/137] B2B-1701: Deprecate database media storage function - Added deprecated tags on related db classes --- .../Magento/MediaStorage/Helper/File/Storage/Database.php | 5 +++++ .../Model/Config/Backend/Storage/Media/Database.php | 4 +++- .../Model/Config/Source/Storage/Media/Database.php | 3 +++ .../Model/Config/Source/Storage/Media/Storage.php | 4 ++++ .../Magento/MediaStorage/Model/File/Storage/Database.php | 2 ++ .../Model/File/Storage/Database/AbstractDatabase.php | 5 ++++- .../MediaStorage/Model/File/Storage/Directory/Database.php | 2 ++ .../Model/ResourceModel/File/Storage/AbstractStorage.php | 2 ++ .../Model/ResourceModel/File/Storage/Database.php | 2 ++ .../Model/ResourceModel/File/Storage/Directory/Database.php | 2 ++ 10 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index 05e2e836abada..ab9bdc84011b7 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -14,6 +14,9 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media storage is deprecated + * */ class Database extends \Magento\Framework\App\Helper\AbstractHelper { @@ -140,6 +143,7 @@ public function getResourceStorageModel() public function saveFile($filename) { if ($this->checkDbUsage()) { + trigger_error('Class is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->saveFile($this->_removeAbsPathFromFileName($filename)); } } @@ -171,6 +175,7 @@ public function renameFile($oldName, $newName) public function copyFile($oldName, $newName) { if ($this->checkDbUsage()) { + trigger_error('Class is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->copyFile( $this->_removeAbsPathFromFileName($oldName), $this->_removeAbsPathFromFileName($newName) diff --git a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php index 1a55be299a75b..131777c9beb94 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php @@ -4,7 +4,9 @@ * See COPYING.txt for license details. */ namespace Magento\MediaStorage\Model\Config\Backend\Storage\Media; - +/** +* @deprecated Database Media storage is deprecated +**/ class Database extends \Magento\Framework\App\Config\Value { /** diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php index 83134c2ac00ef..1d1e98299ee23 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php @@ -12,6 +12,9 @@ use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Config\ConfigOptionsListConstants; +/** + * @deprecated Database Media storage is deprecated + **/ class Database implements \Magento\Framework\Option\ArrayInterface { /** diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php index fb171831407e2..a19e7cd69fbfb 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php @@ -9,6 +9,10 @@ */ namespace Magento\MediaStorage\Model\Config\Source\Storage\Media; + +/** + * @deprecated Database Media storage is deprecated + **/ class Storage implements \Magento\Framework\Option\ArrayInterface { /** diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php index 2bdc69f45ccb8..c11d54523544d 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php @@ -10,6 +10,8 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media storage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php index c9812b86e8b91..34628718fddae 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php @@ -7,7 +7,10 @@ /** * Class AbstractDatabase - */ + * + * @deprecated Database Media storage is deprecated + * + **/ abstract class AbstractDatabase extends \Magento\Framework\Model\AbstractModel { /** diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php index 2617e88e7538a..96db61a6398f3 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php @@ -11,6 +11,8 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media storage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php index 227b428328b79..81cbc44fcf326 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php @@ -7,6 +7,8 @@ /** * Class AbstractStorage + * + * @deprecated Database Media storage is deprecated */ abstract class AbstractStorage extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php index ae896395b8eb5..1fa593d7c832a 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php @@ -10,6 +10,8 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media storage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php index e5f54cac4af6a..6ff011d25d802 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php @@ -7,6 +7,8 @@ /** * Class Database + * + * @deprecated Database Media storage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { From 397e33bcee3a09b606e696fda5430d66c36c2bf5 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Tue, 9 Mar 2021 10:56:06 +0200 Subject: [PATCH 114/137] MC-41069: Free Shipping enables with wrong total for DHL --- app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php index daff459d7ba3f..07da63fa8476e 100644 --- a/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php @@ -226,7 +226,7 @@ public function testGetMethodPrice( ->willReturn($freeShippingEnabled); $request = new RateRequest(); - $request->setBaseSubtotalInclTax($requestSubtotal); + $request->setValueWithDiscount($requestSubtotal); $this->model->setRawRequest($request); $price = $this->model->getMethodPrice($cost, $shippingMethod); $this->assertEquals($expectedPrice, $price); From f802f7c0182ef7bac6306f002fe75b10322440a6 Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Tue, 9 Mar 2021 10:11:22 -0600 Subject: [PATCH 115/137] B2B-1632: Add MFTF test for MC-38948 - Addressing PR feedback --- .../Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php index 530abb1ccbd46..dbf777c1fe5c5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php +++ b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php @@ -227,7 +227,7 @@ public function assertGlobbedFileContainsString($path, $pattern, $text, $fileInd { $realPath = $this->expandPath($path); $files = $this->driver->search($pattern, $realPath); - $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex]), $message); + $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex] ?? ''), $message); } /** From 0fa62d00821810ee2e19de98e57c9cbe4e6e7263 Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Tue, 9 Mar 2021 14:10:50 -0600 Subject: [PATCH 116/137] B2B-1632: Add MFTF test for MC-38948 - Addressing PR feedback --- app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php index 53cb65e9fed5d..cec553afe9463 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php +++ b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php @@ -236,7 +236,7 @@ public function assertFileContainsString($filePath, $text, $message = ""): void public function assertGlobbedFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void { $files = $this->driver->search($pattern, $path); - $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex]), $message); + $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex] ?? ''), $message); } /** From ada9927c1af52836b989cdbb6537269661219398 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Tue, 9 Mar 2021 14:54:53 -0600 Subject: [PATCH 117/137] B2B-1701: Deprecate database media storage function - Added removed trigger errors on class deprecation --- app/code/Magento/MediaStorage/Helper/File/Storage/Database.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index ab9bdc84011b7..61a082fc6c846 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -143,7 +143,6 @@ public function getResourceStorageModel() public function saveFile($filename) { if ($this->checkDbUsage()) { - trigger_error('Class is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->saveFile($this->_removeAbsPathFromFileName($filename)); } } @@ -175,7 +174,6 @@ public function renameFile($oldName, $newName) public function copyFile($oldName, $newName) { if ($this->checkDbUsage()) { - trigger_error('Class is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->copyFile( $this->_removeAbsPathFromFileName($oldName), $this->_removeAbsPathFromFileName($newName) From 61ef125f9b38afbe3a0747fd30c825a0853ff947 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Tue, 9 Mar 2021 15:43:35 -0600 Subject: [PATCH 118/137] B2B-1701: Deprecate database media storage function - Test to verify trigger errors on class deprecation --- app/code/Magento/MediaStorage/Helper/File/Storage/Database.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index 61a082fc6c846..04896961f0c87 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -143,6 +143,7 @@ public function getResourceStorageModel() public function saveFile($filename) { if ($this->checkDbUsage()) { + trigger_error('Database media storage function is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->saveFile($this->_removeAbsPathFromFileName($filename)); } } @@ -174,6 +175,7 @@ public function renameFile($oldName, $newName) public function copyFile($oldName, $newName) { if ($this->checkDbUsage()) { + trigger_error('Database media storage function is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->copyFile( $this->_removeAbsPathFromFileName($oldName), $this->_removeAbsPathFromFileName($newName) From 58f21ca8cd3857b76a7e305a2ca2dc3c8c0ed9d7 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Tue, 9 Mar 2021 23:16:45 -0600 Subject: [PATCH 119/137] B2B-1701: Deprecate database media storage function - Added config file deprecation --- .../MediaStorage/Model/Config/Source/Storage/Media/Storage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php index a19e7cd69fbfb..657188b82e752 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php @@ -27,7 +27,7 @@ public function toOptionArray() 'value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_FILE_SYSTEM, 'label' => __('File System'), ], - ['value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_DATABASE, 'label' => __('Database')] + ['value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_DATABASE, 'label' => __('Database(Deprecated)')] ]; } } From b0e40dee41d4596d2ee9608be5a92e74cb2bccd1 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 10 Mar 2021 10:52:07 +0200 Subject: [PATCH 120/137] MC-41217: [MFTF] CheckingCountryDropDownWithOneAllowedCountryTest fails because of bad design --- ...untryDropDownWithOneAllowedCountryTest.xml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml index 3954318e4e1ee..a22a966af8670 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -11,19 +11,25 @@ <test name="CheckingCountryDropDownWithOneAllowedCountryTest"> <annotations> <features value="Config"/> - <stories value="MAGETWO-96107: Additional blank option in country dropdown"/> + <stories value="Additional blank option in country dropdown"/> <title value="Checking country drop-down with one allowed country"/> <description value="Check country drop-down with one allowed country"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-96133"/> - <group value="configuration"/> + <testCaseId value="MAGETWO-28511"/> + <group value="config"/> </annotations> <before> <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> @@ -47,5 +53,12 @@ <click selector="{{StorefrontCustomerAddressSection.country}}" stepKey="clickToExpandCountryDropDown"/> <see selector="{{StorefrontCustomerAddressSection.country}}" userInput="United States" stepKey="seeSelectedCountry"/> <dontSee selector="{{StorefrontCustomerAddressSection.country}}" userInput="Brazil" stepKey="canNotSeeSelectedCountry"/> + <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> + <reloadPage stepKey="realoadPageAfterConfigChanged"/> + <see selector="{{StorefrontCustomerAddressSection.country}}" userInput="United States" stepKey="seeUnitedStatesCountry"/> + <see selector="{{StorefrontCustomerAddressSection.country}}" userInput="Brazil" stepKey="seeBrazilCountry"/> </test> </tests> From 0415e6483f458645985e9daafca140b48a2f8d03 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 10 Mar 2021 12:35:13 +0200 Subject: [PATCH 121/137] MC-41217: [MFTF] CheckingCountryDropDownWithOneAllowedCountryTest fails because of bad design --- .../StorefrontCustomerSearchBundleProductsByKeywordsTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml index f7bce778cc0d8..268f58c6cb611 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml @@ -41,7 +41,7 @@ </createData> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value="cataloginventory_stock catalog_product_price"/> + <argument name="indices" value=""/> </actionGroup> </before> <after> From 1e5fa8ff03f35a72a3d8fcf4728bd8d743f6ec38 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Wed, 10 Mar 2021 14:41:34 +0200 Subject: [PATCH 122/137] MC-41138: W3C validator reports duplicate ID on Listing Page and Search Result Page --- .../frontend/templates/product/list.phtml | 2 +- .../templates/product/list/toolbar.phtml | 28 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index a831bd7be6f71..4fba22f41c9de 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -153,7 +153,7 @@ $_helper = $block->getData('outputHelper'); <?php endforeach; ?> </ol> </div> - <?= $block->getToolbarHtml() ?> + <?= $block->getChildBlock('toolbar')->setIsBottom(true)->toHtml() ?> <script type="text/x-magento-init"> { "[data-role=tocart-form], .form.map.checkout": { diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml index 76ef6baf4993e..3c8687d090baf 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml @@ -10,27 +10,23 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ - -// phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile -// phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket ?> <?php if ($block->getCollection()->getSize()) :?> <?php $widget = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getWidgetOptionsJson()); $widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['productListToolbarForm']); ?> <div class="toolbar toolbar-products" data-mage-init='{"productListToolbarForm":<?= /* @noEscape */ $widgetOptions ?>}'> - <?php if ($block->isExpanded()) :?> - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> - <?php endif; ?> - - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/amount.phtml')) ?> - - <?= $block->getPagerHtml() ?> - - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?> - - <?php if ($block->isExpanded()) :?> - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?> - <?php endif; ?> + <?php if ($block->getIsBottom()): ?> + <?= $block->getPagerHtml() ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?> + <?php else: ?> + <?php if ($block->isExpanded()): ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> + <?php endif ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/amount.phtml')) ?> + <?php if ($block->isExpanded()): ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?> + <?php endif ?> + <?php endif ?> </div> <?php endif ?> From bfcc680e81f15fc7bfd90c6a351d409acd5044e8 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 10 Mar 2021 16:06:48 +0200 Subject: [PATCH 123/137] MC-41217: [MFTF] CheckingCountryDropDownWithOneAllowedCountryTest fails because of bad design --- .../Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml index a22a966af8670..f65f626f1a520 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -16,7 +16,7 @@ <description value="Check country drop-down with one allowed country"/> <severity value="MAJOR"/> <testCaseId value="MAGETWO-28511"/> - <group value="config"/> + <group value="configuration"/> </annotations> <before> <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> From 3301a80f94d6f431deb0eb4c9756b922ad395ab4 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Wed, 10 Mar 2021 11:30:36 -0600 Subject: [PATCH 124/137] B2B-1701: Deprecate database media storage function - Removed the trigger error --- app/code/Magento/MediaStorage/Helper/File/Storage/Database.php | 2 +- app/code/Magento/MediaStorage/etc/adminhtml/system.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index 04896961f0c87..aae0b86e862b0 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -143,6 +143,7 @@ public function getResourceStorageModel() public function saveFile($filename) { if ($this->checkDbUsage()) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction trigger_error('Database media storage function is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->saveFile($this->_removeAbsPathFromFileName($filename)); } @@ -175,7 +176,6 @@ public function renameFile($oldName, $newName) public function copyFile($oldName, $newName) { if ($this->checkDbUsage()) { - trigger_error('Database media storage function is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->copyFile( $this->_removeAbsPathFromFileName($oldName), $this->_removeAbsPathFromFileName($newName) diff --git a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml index 9c8c2c5b24398..ae825235f88ee 100644 --- a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml +++ b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml @@ -15,7 +15,7 @@ <source_model>Magento\MediaStorage\Model\Config\Source\Storage\Media\Storage</source_model> </field> <field id="media_database" translate="label" type="select" sortOrder="200" showInDefault="1"> - <label>Select Media Database</label> + <label>Select Media Database(Deprecated)</label> <source_model>Magento\MediaStorage\Model\Config\Source\Storage\Media\Database</source_model> <backend_model>Magento\MediaStorage\Model\Config\Backend\Storage\Media\Database</backend_model> <depends> From 6ef4a2ba5e98f2fff82bcb3df577f22d6b69da12 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Wed, 10 Mar 2021 11:42:18 -0600 Subject: [PATCH 125/137] B2B-1701: Deprecate database media storage function - Fixed static test errors --- app/code/Magento/MediaStorage/Helper/File/Storage/Database.php | 2 -- .../MediaStorage/Model/Config/Source/Storage/Media/Storage.php | 2 +- app/code/Magento/MediaStorage/etc/adminhtml/system.xml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index aae0b86e862b0..61a082fc6c846 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -143,8 +143,6 @@ public function getResourceStorageModel() public function saveFile($filename) { if ($this->checkDbUsage()) { - //phpcs:ignore Magento2.Functions.DiscouragedFunction - trigger_error('Database media storage function is deprecated', E_USER_DEPRECATED); $this->getStorageDatabaseModel()->saveFile($this->_removeAbsPathFromFileName($filename)); } } diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php index 657188b82e752..3f3163f61bc67 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php @@ -27,7 +27,7 @@ public function toOptionArray() 'value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_FILE_SYSTEM, 'label' => __('File System'), ], - ['value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_DATABASE, 'label' => __('Database(Deprecated)')] + ['value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_DATABASE, 'label' => __('Database (Deprecated)')] ]; } } diff --git a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml index ae825235f88ee..028243fdb71a1 100644 --- a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml +++ b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml @@ -15,7 +15,7 @@ <source_model>Magento\MediaStorage\Model\Config\Source\Storage\Media\Storage</source_model> </field> <field id="media_database" translate="label" type="select" sortOrder="200" showInDefault="1"> - <label>Select Media Database(Deprecated)</label> + <label>Select Media Database (Deprecated)</label> <source_model>Magento\MediaStorage\Model\Config\Source\Storage\Media\Database</source_model> <backend_model>Magento\MediaStorage\Model\Config\Backend\Storage\Media\Database</backend_model> <depends> From f6f929542b54bbb372f35f19428f600f6ce60398 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Thu, 11 Mar 2021 14:57:27 +0200 Subject: [PATCH 126/137] MC-41138: W3C validator reports duplicate ID on Listing Page and Search Result Page --- .../Catalog/Controller/CategoryTest.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php index 00c3133c25439..4c9f55cf3268d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php @@ -8,14 +8,18 @@ namespace Magento\Catalog\Controller; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Catalog\Model\Session; use Magento\Framework\App\Http\Context; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractController; @@ -53,6 +57,11 @@ class CategoryTest extends AbstractController */ private $httpContext; + /** + * @var CollectionFactory + */ + private $categoryCollectionFactory; + /** * @inheritdoc */ @@ -64,6 +73,8 @@ protected function setUp(): void $this->objectManager->configure([ 'preferences' => [LayoutUpdateManager::class => CategoryLayoutUpdateManager::class] ]); + + $this->categoryCollectionFactory = $this->objectManager->create(CollectionFactory::class); $this->registry = $this->objectManager->get(Registry::class); $this->layout = $this->objectManager->get(LayoutInterface::class); $this->session = $this->objectManager->get(Session::class); @@ -233,4 +244,50 @@ public function testViewWithRememberPaginationAndPreviousValue(): void $this->assertEquals($newPaginationValue, $this->session->getData(ToolbarModel::LIMIT_PARAM_NAME)); $this->assertEquals($newPaginationValue, $this->httpContext->getValue(ToolbarModel::LIMIT_PARAM_NAME)); } + + /** + * Test to generate category page without duplicate html element ids + * + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @magentoDataFixture Magento/Catalog/_files/catalog_category_product_reindex_all.php + * @magentoDataFixture Magento/Catalog/_files/catalog_product_category_reindex_all.php + * @magentoDbIsolation disabled + */ + public function testViewWithoutDuplicateHmlElementIds(): void + { + $category = $this->loadCategory('Category 999', Store::DEFAULT_STORE_ID); + $this->dispatch('catalog/category/view/id/' . $category->getId()); + + $responseHtml = $this->getResponse()->getBody(); + $htmlElementIds = ['modes-label', 'mode-list', 'toolbar-amount', 'sorter', 'limiter']; + foreach ($htmlElementIds as $elementId) { + $matches = []; + $idAttribute = "id=\"$elementId\""; + preg_match_all("/$idAttribute/mx", $responseHtml, $matches); + $this->assertCount(1, $matches[0]); + $this->assertEquals($idAttribute, $matches[0][0]); + } + } + + /** + * Loads category by id + * + * @param string $categoryName + * @param int $storeId + * @return CategoryInterface + */ + protected function loadCategory(string $categoryName, int $storeId): CategoryInterface + { + /** @var Collection $categoryCollection */ + $categoryCollection = $this->categoryCollectionFactory->create(); + /** @var CategoryInterface $category */ + $category = $categoryCollection->setStoreId($storeId) + ->addAttributeToSelect('display_mode', 'left') + ->addAttributeToFilter(CategoryInterface::KEY_NAME, $categoryName) + ->setPageSize(1) + ->getFirstItem(); + $category->setStoreId($storeId); + + return $category; + } } From 0e92df509333b941dd257c8a2f84b9b4ac398f20 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transogtgroup.com> Date: Thu, 11 Mar 2021 15:59:16 +0200 Subject: [PATCH 127/137] MC-41138: W3C validator reports duplicate ID on Listing Page and Search Result Page --- .../testsuite/Magento/Catalog/Controller/CategoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php index 4c9f55cf3268d..36bca76b28de1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php @@ -276,7 +276,7 @@ public function testViewWithoutDuplicateHmlElementIds(): void * @param int $storeId * @return CategoryInterface */ - protected function loadCategory(string $categoryName, int $storeId): CategoryInterface + private function loadCategory(string $categoryName, int $storeId): CategoryInterface { /** @var Collection $categoryCollection */ $categoryCollection = $this->categoryCollectionFactory->create(); From 8b76bf9f4eca450d4c52159a0d3a2bf53dae13f0 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Thu, 11 Mar 2021 10:47:28 -0600 Subject: [PATCH 128/137] B2B-1701: Deprecate database media storage function - Fixed review comments --- .../Magento/MediaStorage/Helper/File/Storage/Database.php | 2 +- .../Model/Config/Backend/Storage/Media/Database.php | 2 +- .../Model/Config/Source/Storage/Media/Database.php | 2 +- .../Model/Config/Source/Storage/Media/Storage.php | 4 ---- app/code/Magento/MediaStorage/Model/File/Storage/Database.php | 2 +- .../Model/File/Storage/Database/AbstractDatabase.php | 2 +- .../MediaStorage/Model/File/Storage/Directory/Database.php | 2 +- .../Model/ResourceModel/File/Storage/AbstractStorage.php | 2 +- .../Model/ResourceModel/File/Storage/Database.php | 2 +- .../Model/ResourceModel/File/Storage/Directory/Database.php | 2 +- 10 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index 61a082fc6c846..e9823720a2166 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -15,7 +15,7 @@ * @api * @since 100.0.2 * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated * */ class Database extends \Magento\Framework\App\Helper\AbstractHelper diff --git a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php index 131777c9beb94..865af7039cd7f 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php @@ -5,7 +5,7 @@ */ namespace Magento\MediaStorage\Model\Config\Backend\Storage\Media; /** -* @deprecated Database Media storage is deprecated +* @deprecated Database MediaStorage is deprecated **/ class Database extends \Magento\Framework\App\Config\Value { diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php index 1d1e98299ee23..565c79aa26917 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php @@ -13,7 +13,7 @@ use Magento\Framework\Config\ConfigOptionsListConstants; /** - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated **/ class Database implements \Magento\Framework\Option\ArrayInterface { diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php index 3f3163f61bc67..c881b59c5c593 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php @@ -9,10 +9,6 @@ */ namespace Magento\MediaStorage\Model\Config\Source\Storage\Media; - -/** - * @deprecated Database Media storage is deprecated - **/ class Storage implements \Magento\Framework\Option\ArrayInterface { /** diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php index c11d54523544d..304ac9f0d766c 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php @@ -11,7 +11,7 @@ * @api * @since 100.0.2 * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php index 34628718fddae..9ffe8a4ed9871 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php @@ -8,7 +8,7 @@ /** * Class AbstractDatabase * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated * **/ abstract class AbstractDatabase extends \Magento\Framework\Model\AbstractModel diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php index 96db61a6398f3..114534ba08c0c 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php index 81cbc44fcf326..3d6d2e851d877 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php @@ -8,7 +8,7 @@ /** * Class AbstractStorage * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated */ abstract class AbstractStorage extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php index 1fa593d7c832a..b66edf821cdb5 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php @@ -11,7 +11,7 @@ * @api * @since 100.0.2 * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php index 6ff011d25d802..1d9e38279ed91 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php @@ -8,7 +8,7 @@ /** * Class Database * - * @deprecated Database Media storage is deprecated + * @deprecated Database MediaStorage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { From 632d196d4354fb767c9b00851b76899ceb48282a Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Thu, 11 Mar 2021 11:17:19 -0600 Subject: [PATCH 129/137] B2B-1701: Deprecate database media storage function - Fixed space on comments --- app/code/Magento/MediaStorage/Helper/File/Storage/Database.php | 2 +- .../Model/Config/Backend/Storage/Media/Database.php | 2 +- .../MediaStorage/Model/Config/Source/Storage/Media/Database.php | 2 +- app/code/Magento/MediaStorage/Model/File/Storage/Database.php | 2 +- .../Model/File/Storage/Database/AbstractDatabase.php | 2 +- .../Model/ResourceModel/File/Storage/AbstractStorage.php | 2 +- .../MediaStorage/Model/ResourceModel/File/Storage/Database.php | 2 +- .../Model/ResourceModel/File/Storage/Directory/Database.php | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index e9823720a2166..e0f5509830a0c 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -15,7 +15,7 @@ * @api * @since 100.0.2 * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated * */ class Database extends \Magento\Framework\App\Helper\AbstractHelper diff --git a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php index 865af7039cd7f..81e6c5e028be1 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php @@ -5,7 +5,7 @@ */ namespace Magento\MediaStorage\Model\Config\Backend\Storage\Media; /** -* @deprecated Database MediaStorage is deprecated +* @deprecated Database Media Storage is deprecated **/ class Database extends \Magento\Framework\App\Config\Value { diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php index 565c79aa26917..97f161e4c49d8 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php @@ -13,7 +13,7 @@ use Magento\Framework\Config\ConfigOptionsListConstants; /** - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated **/ class Database implements \Magento\Framework\Option\ArrayInterface { diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php index 304ac9f0d766c..571dad7f0ae9a 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php @@ -11,7 +11,7 @@ * @api * @since 100.0.2 * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php index 9ffe8a4ed9871..3528ca5743ff7 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php @@ -8,7 +8,7 @@ /** * Class AbstractDatabase * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated * **/ abstract class AbstractDatabase extends \Magento\Framework\Model\AbstractModel diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php index 3d6d2e851d877..0533c0229ea3d 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php @@ -8,7 +8,7 @@ /** * Class AbstractStorage * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated */ abstract class AbstractStorage extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php index b66edf821cdb5..863b368883fbe 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php @@ -11,7 +11,7 @@ * @api * @since 100.0.2 * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php index 1d9e38279ed91..342761646e396 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php @@ -8,7 +8,7 @@ /** * Class Database * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { From 4fc396a18ee01a697a637c09412a682632936c2d Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Thu, 11 Mar 2021 11:18:26 -0600 Subject: [PATCH 130/137] B2B-1701: Deprecate database media storage function - Fixed space on comments on missed dir file --- .../MediaStorage/Model/File/Storage/Directory/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php index 114534ba08c0c..03d5eee617b1b 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated Database MediaStorage is deprecated + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { From bc5f97e65aadf8c8e5ee216100805874826198ca Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <svizev.igor@gmail.com> Date: Fri, 12 Mar 2021 14:33:13 +0200 Subject: [PATCH 131/137] Fix incorrect setting of the SameSite cookie param --- .../Cookie/view/base/web/js/jquery.storageapi.extended.js | 6 ++++-- lib/web/jquery/jquery.cookie.js | 2 +- lib/web/mage/cookies.js | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js b/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js index dfbd70f477a67..c165866522642 100644 --- a/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js +++ b/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js @@ -16,9 +16,11 @@ define([ * @private */ function _extend(storage) { + var cookiesConfig = window.cookiesConfig || {}; + $.extend(storage, { - _secure: window.cookiesConfig ? window.cookiesConfig.secure : false, - _samesite: window.cookiesConfig ? window.cookiesConfig.samesite : 'lax', + _secure: !!cookiesConfig.secure, + _samesite: cookiesConfig.samesite ? cookiesConfig.samesite : 'lax', /** * Set value under name diff --git a/lib/web/jquery/jquery.cookie.js b/lib/web/jquery/jquery.cookie.js index 973e8ad9b7925..654b4619fdb43 100644 --- a/lib/web/jquery/jquery.cookie.js +++ b/lib/web/jquery/jquery.cookie.js @@ -47,7 +47,7 @@ options.path ? '; path=' + options.path : '', options.domain ? '; domain=' + options.domain : '', options.secure ? '; secure' : '', - options.samesite ? '; samesite=' + options.samesite : 'lax', + '; samesite=' + (options.samesite ? options.samesite : 'lax'), ].join('')); } diff --git a/lib/web/mage/cookies.js b/lib/web/mage/cookies.js index 317c396096087..3e42ff9c404c7 100644 --- a/lib/web/mage/cookies.js +++ b/lib/web/mage/cookies.js @@ -76,7 +76,7 @@ define([ (path ? '; path=' + path : '') + (domain ? '; domain=' + domain : '') + (secure ? '; secure' : '') + - (samesite ? '; samesite=' + samesite : 'lax'); + '; samesite=' + (samesite ? samesite : 'lax'); }; /** From 41b935d9645f7108232cef2f39a521cd22d18623 Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <svizev.igor@gmail.com> Date: Fri, 12 Mar 2021 14:34:03 +0200 Subject: [PATCH 132/137] Fix incorrect setting of the SameSite cookie param Add samesite cookie param support in form key provider and admin tools --- .../PageCache/view/frontend/web/js/form-key-provider.js | 7 +++++-- lib/web/mage/adminhtml/tools.js | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js index c63d97840e946..4db05d0bd2772 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js @@ -17,14 +17,17 @@ define(function () { function setFormKeyCookie(value) { var expires, secure, + samesite, date = new Date(), - isSecure = !!window.cookiesConfig && window.cookiesConfig.secure; + cookiesConfig = window.cookiesConfig || {}, + isSecure = !!cookiesConfig.secure; date.setTime(date.getTime() + 86400000); expires = '; expires=' + date.toUTCString(); secure = isSecure ? '; secure' : ''; + samesite = '; samesite=' + (cookiesConfig.samesite ? cookiesConfig.samesite : 'lax'); - document.cookie = 'form_key=' + (value || '') + expires + secure + '; path=/'; + document.cookie = 'form_key=' + (value || '') + expires + secure + '; path=/' + samesite; } /** diff --git a/lib/web/mage/adminhtml/tools.js b/lib/web/mage/adminhtml/tools.js index 27f6efcfc5876..12fe88bb171a4 100644 --- a/lib/web/mage/adminhtml/tools.js +++ b/lib/web/mage/adminhtml/tools.js @@ -267,7 +267,7 @@ var Cookie = { return null; }, - write: function (cookieName, cookieValue, cookieLifeTime) { + write: function (cookieName, cookieValue, cookieLifeTime, samesite) { var expires = ''; if (cookieLifeTime) { @@ -278,7 +278,9 @@ var Cookie = { } var urlPath = '/' + BASE_URL.split('/').slice(3).join('/'); // Get relative path - document.cookie = escape(cookieName) + '=' + escape(cookieValue) + expires + '; path=' + urlPath; + samesite = '; samesite=' + (samesite ? samesite : 'lax'); + + document.cookie = escape(cookieName) + '=' + escape(cookieValue) + expires + '; path=' + urlPath + samesite; }, clear: function (cookieName) { this.write(cookieName, '', -1); From 35570b3b0a98b578f88bd6cb694b1c0f4fc2b7c8 Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <ihor-sviziev@users.noreply.github.com> Date: Fri, 12 Mar 2021 15:35:46 +0200 Subject: [PATCH 133/137] Fix incorrect setting of the SameSite cookie param Apply suggestions from code review Co-authored-by: Denis Kopylov <dkopylov@magenius.team> --- .../PageCache/view/frontend/web/js/form-key-provider.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js index 4db05d0bd2772..b3d3f2abca8ea 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js @@ -17,15 +17,15 @@ define(function () { function setFormKeyCookie(value) { var expires, secure, - samesite, date = new Date(), cookiesConfig = window.cookiesConfig || {}, isSecure = !!cookiesConfig.secure; + samesite = cookiesConfig.samesite || 'lax'; date.setTime(date.getTime() + 86400000); expires = '; expires=' + date.toUTCString(); secure = isSecure ? '; secure' : ''; - samesite = '; samesite=' + (cookiesConfig.samesite ? cookiesConfig.samesite : 'lax'); + samesite = '; samesite=' + samesite; document.cookie = 'form_key=' + (value || '') + expires + secure + '; path=/' + samesite; } From abdeb4e7729d8dfea6cb94b772c7ffbb75e52c7f Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Fri, 12 Mar 2021 07:43:08 -0600 Subject: [PATCH 134/137] B2B-1701: Deprecate database media storage function - Added test new changes --- .../Magento/MediaStorage/Helper/File/Storage/Database.php | 1 + .../Test/Unit/Helper/File/Storage/DatabaseTest.php | 6 ++++++ app/code/Magento/MediaStorage/etc/adminhtml/system.xml | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index e0f5509830a0c..ce5d9cf36002d 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +trigger_error('Class is deprecated', E_USER_DEPRECATED); /** * Database saving file helper diff --git a/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php b/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php index 0a7038838cf97..fed116e6db1a1 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php @@ -49,6 +49,7 @@ class DatabaseTest extends TestCase protected function setUp(): void { + set_error_handler(null); $this->dbStorageFactoryMock = $this->getMockBuilder( DatabaseFactory::class )->disableOriginalConstructor() @@ -519,4 +520,9 @@ public function testGetMediaBaseDir() $this->assertEquals('media-dir', $this->helper->getMediaBaseDir()); $this->assertEquals('media-dir', $this->helper->getMediaBaseDir()); } + + protected function tearDown(): void + { + restore_error_handler(); + } } diff --git a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml index 028243fdb71a1..9c8c2c5b24398 100644 --- a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml +++ b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml @@ -15,7 +15,7 @@ <source_model>Magento\MediaStorage\Model\Config\Source\Storage\Media\Storage</source_model> </field> <field id="media_database" translate="label" type="select" sortOrder="200" showInDefault="1"> - <label>Select Media Database (Deprecated)</label> + <label>Select Media Database</label> <source_model>Magento\MediaStorage\Model\Config\Source\Storage\Media\Database</source_model> <backend_model>Magento\MediaStorage\Model\Config\Backend\Storage\Media\Database</backend_model> <depends> From 753be2a3db726090a6f05b7801ed4047ee65f36d Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <ihor-sviziev@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:18:20 +0200 Subject: [PATCH 135/137] Fix incorrect setting of the SameSite cookie param Fix typo Co-authored-by: Denis Kopylov <dkopylov@magenius.team> --- .../Magento/PageCache/view/frontend/web/js/form-key-provider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js index b3d3f2abca8ea..f602166e34143 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js @@ -19,7 +19,7 @@ define(function () { secure, date = new Date(), cookiesConfig = window.cookiesConfig || {}, - isSecure = !!cookiesConfig.secure; + isSecure = !!cookiesConfig.secure, samesite = cookiesConfig.samesite || 'lax'; date.setTime(date.getTime() + 86400000); From 313d1eefab33d32761dc354882bb2fe38865974d Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Fri, 12 Mar 2021 08:38:43 -0600 Subject: [PATCH 136/137] B2B-1701: Deprecate database media storage function - removed trigger error on db file --- app/code/Magento/MediaStorage/Helper/File/Storage/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index ce5d9cf36002d..e0f5509830a0c 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -8,7 +8,6 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; -trigger_error('Class is deprecated', E_USER_DEPRECATED); /** * Database saving file helper From 4f7121ebd0b3fc3a281b33c2c0e11341d4284cf3 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Fri, 12 Mar 2021 11:27:31 -0600 Subject: [PATCH 137/137] B2B-1701: Deprecate database media storage function - removed test changes as not required --- .../Test/Unit/Helper/File/Storage/DatabaseTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php b/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php index fed116e6db1a1..0a7038838cf97 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/Helper/File/Storage/DatabaseTest.php @@ -49,7 +49,6 @@ class DatabaseTest extends TestCase protected function setUp(): void { - set_error_handler(null); $this->dbStorageFactoryMock = $this->getMockBuilder( DatabaseFactory::class )->disableOriginalConstructor() @@ -520,9 +519,4 @@ public function testGetMediaBaseDir() $this->assertEquals('media-dir', $this->helper->getMediaBaseDir()); $this->assertEquals('media-dir', $this->helper->getMediaBaseDir()); } - - protected function tearDown(): void - { - restore_error_handler(); - } }