From 405dfec0026e6be2c11281d31774f08a293ded2d Mon Sep 17 00:00:00 2001 From: Stanislav Ilnytskyi Date: Fri, 14 Feb 2020 14:00:50 +0100 Subject: [PATCH 01/96] Product not exist fix when attempt to get an image --- app/code/Magento/Catalog/Helper/Image.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 5b0aa0c496ecd..085fb2c224b14 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -514,7 +514,7 @@ protected function initBaseFile() if ($this->getImageFile()) { $model->setBaseFile($this->getImageFile()); } else { - $model->setBaseFile($this->getProduct()->getData($model->getDestinationSubdir())); + $model->setBaseFile($this->getProduct() ? $this->getProduct()->getData($model->getDestinationSubdir()) : ''); } } return $this; From 652971e5b1cdd6df545b6c8a99cd1c306b0a10d9 Mon Sep 17 00:00:00 2001 From: Alex Taranovsky Date: Sun, 22 Mar 2020 21:04:47 +0200 Subject: [PATCH 02/96] magento/magento2#: Add a new index for `cron_schedule` table --- app/code/Magento/Cron/etc/db_schema.xml | 4 ++++ app/code/Magento/Cron/etc/db_schema_whitelist.json | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index 206b8f64f3ae7..c689baf0f4bca 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -28,5 +28,9 @@ + + + + diff --git a/app/code/Magento/Cron/etc/db_schema_whitelist.json b/app/code/Magento/Cron/etc/db_schema_whitelist.json index c8666896627e2..f0d6ebed8290f 100644 --- a/app/code/Magento/Cron/etc/db_schema_whitelist.json +++ b/app/code/Magento/Cron/etc/db_schema_whitelist.json @@ -12,10 +12,11 @@ }, "index": { "CRON_SCHEDULE_JOB_CODE": true, - "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true + "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true, + "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true }, "constraint": { "PRIMARY": true } } -} \ No newline at end of file +} From 2f8b560ab3801595d8e78b2f4c696c8c75974e9b Mon Sep 17 00:00:00 2001 From: Gaurav Agarwal <37572719+gauravagarwal1001@users.noreply.github.com> Date: Sun, 5 Apr 2020 03:42:02 +0530 Subject: [PATCH 03/96] Fix Issue #27350 --- .../templates/sales/invoice/create/items/renderer.phtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index a7d49b4b3530a..f8dc1904288e0 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -16,7 +16,7 @@ getChildren($_item); ?> - + getOrderOptions() || $_item->getDescription()) : ?> @@ -34,7 +34,7 @@ getOrderItem()->getParentItem()) : ?> getSelectionAttributes($_item) ?> @@ -124,7 +124,7 @@ canShowPriceInfo($_item) || $shipTogether) : ?> - canEditQty()) : ?> + canEditQty() && $canEditItemQty) : ?> Date: Sun, 12 Apr 2020 17:38:41 +0200 Subject: [PATCH 04/96] Edits for bug magento/magento2/#11998 --- app/code/Magento/Catalog/Block/Product/View.php | 4 ++++ .../Block/Product/View/Type/Configurable.php | 5 +++++ .../Product/Type/Configurable/Variations/Prices.php | 3 +++ app/code/Magento/Tax/Pricing/Render/Adjustment.php | 13 +++++++++++++ .../view/base/templates/pricing/adjustment.phtml | 7 ++++--- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php index 437171bcb4bc6..232788d79ae9f 100644 --- a/app/code/Magento/Catalog/Block/Product/View.php +++ b/app/code/Magento/Catalog/Block/Product/View.php @@ -196,6 +196,10 @@ public function getJsonConfig() 'productId' => (int)$product->getId(), 'priceFormat' => $this->_localeFormat->getPriceFormat(), 'prices' => [ + 'baseOldPrice' => [ + 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() * 1, + 'adjustments' => [] + ], 'oldPrice' => [ 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getValue() * 1, 'adjustments' => [] diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index 55c0c8f6ca4ce..f3c3ed55a5d3a 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -303,6 +303,11 @@ protected function getOptionPrices() $prices[$product->getId()] = [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() + ), + ], 'oldPrice' => [ 'amount' => $this->localeFormat->getNumber( $priceInfo->getPrice('regular_price')->getAmount()->getValue() diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php index b8a948d55f11a..492c5de55ad7f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php @@ -39,6 +39,9 @@ public function getFormattedPrices(\Magento\Framework\Pricing\PriceInfo\Base $pr $finalPrice = $priceInfo->getPrice('final_price'); return [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getBaseAmount()), + ], 'oldPrice' => [ 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getValue()), ], diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php index 8613e62f2983e..6141b958bbaa1 100644 --- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php +++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php @@ -173,4 +173,17 @@ public function displayPriceExcludingTax() { return $this->taxHelper->displayPriceExcludingTax(); } + + /** + * Obtain a value for data-price-type attribute + * + * @return string + */ + public function getDataPriceType() + { + if ( $this->getData('price_type') !== 'finalPrice' && $priceType = $this->getData('price_type')) { + return 'base' . ucfirst($priceType); + } + return 'basePrice'; + } } diff --git a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml index e87d1c9eb96aa..5db066eb5e2fd 100644 --- a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml +++ b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml @@ -6,12 +6,13 @@ ?> + displayBothPrices()) : ?> - getDisplayAmountExclTax() ?> From 7f4b2766b8b068ca03595d0548e84ed94c47773b Mon Sep 17 00:00:00 2001 From: Jack Krielen Date: Mon, 13 Apr 2020 12:30:23 +0200 Subject: [PATCH 05/96] Static error fixes (code styling) --- app/code/Magento/Tax/Pricing/Render/Adjustment.php | 4 +++- .../Magento/Tax/view/base/templates/pricing/adjustment.phtml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php index 6141b958bbaa1..c2c3a24d59535 100644 --- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php +++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php @@ -38,6 +38,8 @@ public function __construct( } /** + * Apply the right HTML output to the adjustment + * * @return string */ protected function apply() @@ -181,7 +183,7 @@ public function displayPriceExcludingTax() */ public function getDataPriceType() { - if ( $this->getData('price_type') !== 'finalPrice' && $priceType = $this->getData('price_type')) { + if ( $this->getData('price_type') !== 'finalPrice' && $priceType = $this->getData('price_type')){ return 'base' . ucfirst($priceType); } return 'basePrice'; diff --git a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml index 5db066eb5e2fd..685893151bc5a 100644 --- a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml +++ b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml @@ -8,7 +8,7 @@ -displayBothPrices()) : ?> +displayBothPrices()): ?> Date: Tue, 14 Apr 2020 08:50:01 +0200 Subject: [PATCH 06/96] Refactor small line of code. + Code Styling. --- app/code/Magento/Tax/Pricing/Render/Adjustment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php index c2c3a24d59535..ec451eb012878 100644 --- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php +++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php @@ -183,8 +183,8 @@ public function displayPriceExcludingTax() */ public function getDataPriceType() { - if ( $this->getData('price_type') !== 'finalPrice' && $priceType = $this->getData('price_type')){ - return 'base' . ucfirst($priceType); + if ($this->getData('price_type') && $this->getData('price_type') !== 'finalPrice') { + return 'base' . ucfirst($this->getData('price_type')); } return 'basePrice'; } From 8f1020af9f5cca737bb24696769f7eb81958556a Mon Sep 17 00:00:00 2001 From: Jack Krielen Date: Wed, 15 Apr 2020 19:39:19 +0200 Subject: [PATCH 07/96] Fix testing. --- .../Unit/Block/Product/View/Type/ConfigurableTest.php | 9 +++++++++ .../Product/Type/Configurable/Variations/PricesTest.php | 7 +++++-- .../Block/Product/Renderer/Configurable/PriceTest.php | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 36fda4ef3245c..7fd66541adc7b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -323,6 +323,9 @@ public function testGetJsonConfig() ->with($priceInfoMock) ->willReturn( [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -362,6 +365,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): 'currencyFormat' => '%s', 'optionPrices' => [ $productId => [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -385,6 +391,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): ], 'priceFormat' => [], 'prices' => [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php index b4e7689498fe6..cab1e8f45e659 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php @@ -33,6 +33,9 @@ protected function setUp() public function testGetFormattedPrices() { $expected = [ + 'baseOldPrice' => [ + 'amount' => 1000 + ], 'oldPrice' => [ 'amount' => 500 ], @@ -54,8 +57,8 @@ public function testGetFormattedPrices() $this->localeFormatMock->expects($this->atLeastOnce()) ->method('getNumber') - ->withConsecutive([500], [1000], [500]) - ->will($this->onConsecutiveCalls(500, 1000, 500)); + ->withConsecutive([1000],[500], [1000], [500]) + ->will($this->onConsecutiveCalls(1000,500, 1000, 500)); $this->assertEquals($expected, $this->model->getFormattedPrices($priceInfoMock)); } diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php index 2c5bf805f92de..75834b8ba6c28 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php @@ -116,6 +116,7 @@ public function childProductsDataProvider(): array ], 'expected_data' => [ [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 50], 'finalPrice' => ['amount' => 50], @@ -123,6 +124,7 @@ public function childProductsDataProvider(): array 'msrpPrice' => ['amount' => null], ], [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 58.55], 'finalPrice' => ['amount' => 58.55], @@ -130,6 +132,7 @@ public function childProductsDataProvider(): array 'msrpPrice' => ['amount' => null], ], [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 75], 'finalPrice' => ['amount' => 75], From cfb762d664a2da0933ee1a6fc46deb684fc6eecd Mon Sep 17 00:00:00 2001 From: Jack Krielen Date: Thu, 16 Apr 2020 09:33:04 +0200 Subject: [PATCH 08/96] Fix testing. --- .../Model/Product/Type/Configurable/Variations/PricesTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php index cab1e8f45e659..1e8821217f2b1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php @@ -57,8 +57,8 @@ public function testGetFormattedPrices() $this->localeFormatMock->expects($this->atLeastOnce()) ->method('getNumber') - ->withConsecutive([1000],[500], [1000], [500]) - ->will($this->onConsecutiveCalls(1000,500, 1000, 500)); + ->withConsecutive([1000], [500], [1000], [500]) + ->will($this->onConsecutiveCalls(1000, 500, 1000, 500)); $this->assertEquals($expected, $this->model->getFormattedPrices($priceInfoMock)); } From c6a43f3920d29c046ca7a097e520f30f72f14ac7 Mon Sep 17 00:00:00 2001 From: Jack Krielen Date: Thu, 16 Apr 2020 14:39:01 +0200 Subject: [PATCH 09/96] New Testing format like #27500 --- .../Product/View/Type/ConfigurableTest.php | 52 +++++++++---------- .../Configurable/Variations/PricesTest.php | 6 +-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 7fd66541adc7b..9ab288030cdd0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -15,79 +15,79 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Block\Product\Context|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Block\Product\Context|\PHPUnit\Framework\MockObject\MockObject */ private $context; /** - * @var \Magento\Framework\Stdlib\ArrayUtils|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Stdlib\ArrayUtils|\PHPUnit\Framework\MockObject\MockObject */ private $arrayUtils; /** - * @var \Magento\Framework\Json\EncoderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Json\EncoderInterface|\PHPUnit\Framework\MockObject\MockObject */ private $jsonEncoder; /** - * @var \Magento\ConfigurableProduct\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\ConfigurableProduct\Helper\Data|\PHPUnit\Framework\MockObject\MockObject */ private $helper; /** - * @var \Magento\Catalog\Helper\Product|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Product|\PHPUnit\Framework\MockObject\MockObject */ private $product; /** - * @var \Magento\Customer\Helper\Session\CurrentCustomer|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Helper\Session\CurrentCustomer|\PHPUnit\Framework\MockObject\MockObject */ private $currentCustomer; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit\Framework\MockObject\MockObject */ private $priceCurrency; /** - * @var \Magento\Directory\Model\Currency|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Directory\Model\Currency|\PHPUnit\Framework\MockObject\MockObject */ private $currency; /** - * @var \Magento\ConfigurableProduct\Model\ConfigurableAttributeData|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\ConfigurableProduct\Model\ConfigurableAttributeData|\PHPUnit\Framework\MockObject\MockObject */ private $configurableAttributeData; /** - * @var \Magento\Framework\Locale\Format|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Locale\Format|\PHPUnit\Framework\MockObject\MockObject */ private $localeFormat; /** - * @var \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable|\PHPUnit\Framework\MockObject\MockObject */ private $block; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit\Framework\MockObject\MockObject */ private $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit\Framework\MockObject\MockObject */ private $customerSession; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit\Framework\MockObject\MockObject */ private $variationPricesMock; /** * {@inheritDoc} */ - protected function setUp() + protected function setUp(): void { $this->mockContextObject(); @@ -230,7 +230,7 @@ public function cacheKeyProvider(): array * @param string|null $priceCurrency * @param string|null $customerGroupId */ - public function testGetCacheKeyInfo(array $expected, string $priceCurrency = null, string $customerGroupId = null) + public function testGetCacheKeyInfo(array $expected, string $priceCurrency = null, string $customerGroupId = null): void { $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) ->setMethods(['getCurrentCurrency']) @@ -258,7 +258,7 @@ public function testGetCacheKeyInfo(array $expected, string $priceCurrency = nul /** * Check that getJsonConfig() method returns expected value */ - public function testGetJsonConfig() + public function testGetJsonConfig(): void { $productId = 1; $amount = 10.50; @@ -416,10 +416,10 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): /** * Retrieve mocks of \Magento\ConfigurableProduct\Model\Product\Type\Configurable object * - * @param \PHPUnit_Framework_MockObject_MockObject $productMock - * @return \PHPUnit_Framework_MockObject_MockObject + * @param \PHPUnit\Framework\MockObject\MockObject $productMock + * @return \PHPUnit\Framework\MockObject\MockObject */ - private function getProductTypeMock(\PHPUnit_Framework_MockObject_MockObject $productMock) + private function getProductTypeMock(\PHPUnit\Framework\MockObject\MockObject $productMock): \PHPUnit\Framework\MockObject\MockObject { $currencyMock = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) ->disableOriginalConstructor() @@ -459,7 +459,7 @@ private function getProductTypeMock(\PHPUnit_Framework_MockObject_MockObject $pr * * @return void */ - protected function mockContextObject() + protected function mockContextObject(): void { $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->getMockForAbstractClass(); @@ -476,9 +476,9 @@ protected function mockContextObject() * Retrieve mock of \Magento\Framework\Pricing\Amount\AmountInterface object * * @param float $amount - * @return \PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Framework\Pricing\Amount\AmountInterface|\PHPUnit\Framework\MockObject\MockObject */ - protected function getAmountMock($amount): \PHPUnit_Framework_MockObject_MockObject + protected function getAmountMock($amount): \PHPUnit\Framework\MockObject\MockObject { $amountMock = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) ->setMethods(['getValue', 'getBaseAmount']) @@ -496,12 +496,12 @@ protected function getAmountMock($amount): \PHPUnit_Framework_MockObject_MockObj /** * Retrieve mock of \Magento\Catalog\Pricing\Price\TierPriceInterface object * - * @param \PHPUnit_Framework_MockObject_MockObject $amountMock + * @param \PHPUnit\Framework\MockObject\MockObject $amountMock * @param float $priceQty * @param int $percentage - * @return \PHPUnit_Framework_MockObject_MockObject + * @return \PHPUnit\Framework\MockObject\MockObject */ - protected function getTierPriceMock(\PHPUnit_Framework_MockObject_MockObject $amountMock, $priceQty, $percentage) + protected function getTierPriceMock(\PHPUnit_Framework_MockObject_MockObject $amountMock, $priceQty, $percentage): \PHPUnit\Framework\MockObject\MockObject { $tierPrice = [ 'price_qty' => $priceQty, diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php index 1e8821217f2b1..c91c681d0458e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php @@ -13,7 +13,7 @@ class PricesTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit\Framework\MockObject\MockObject */ private $localeFormatMock; @@ -22,7 +22,7 @@ class PricesTest extends TestCase */ private $model; - protected function setUp() + protected function setUp(): void { $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\Format::class); $this->model = new \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices( @@ -30,7 +30,7 @@ protected function setUp() ); } - public function testGetFormattedPrices() + public function testGetFormattedPrices(): void { $expected = [ 'baseOldPrice' => [ From 84124f74b55a88b09947f1fc561a4ad30ecdd07b Mon Sep 17 00:00:00 2001 From: Jack Krielen Date: Thu, 16 Apr 2020 16:16:17 +0200 Subject: [PATCH 10/96] Code Styling, Fixing --- .../Product/View/Type/ConfigurableTest.php | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 9ab288030cdd0..475da7eb00daa 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -230,8 +230,11 @@ public function cacheKeyProvider(): array * @param string|null $priceCurrency * @param string|null $customerGroupId */ - public function testGetCacheKeyInfo(array $expected, string $priceCurrency = null, string $customerGroupId = null): void - { + public function testGetCacheKeyInfo( + array $expected, + string $priceCurrency = null, + string $customerGroupId = null + ): void { $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) ->setMethods(['getCurrentCurrency']) ->getMockForAbstractClass(); @@ -385,7 +388,7 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): ], ], 'msrpPrice' => [ - 'amount' => null , + 'amount' => null, ] ], ], @@ -419,8 +422,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): * @param \PHPUnit\Framework\MockObject\MockObject $productMock * @return \PHPUnit\Framework\MockObject\MockObject */ - private function getProductTypeMock(\PHPUnit\Framework\MockObject\MockObject $productMock): \PHPUnit\Framework\MockObject\MockObject - { + private function getProductTypeMock( + \PHPUnit\Framework\MockObject\MockObject $productMock + ): \PHPUnit\Framework\MockObject\MockObject { $currencyMock = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) ->disableOriginalConstructor() ->getMock(); @@ -501,8 +505,11 @@ protected function getAmountMock($amount): \PHPUnit\Framework\MockObject\MockObj * @param int $percentage * @return \PHPUnit\Framework\MockObject\MockObject */ - protected function getTierPriceMock(\PHPUnit_Framework_MockObject_MockObject $amountMock, $priceQty, $percentage): \PHPUnit\Framework\MockObject\MockObject - { + protected function getTierPriceMock( + \PHPUnit\Framework\MockObject\MockObject $amountMock, + $priceQty, + $percentage + ): \PHPUnit\Framework\MockObject\MockObject { $tierPrice = [ 'price_qty' => $priceQty, 'price' => $amountMock, From 674324c6ae3cdee5c664bac6be3e275070bda758 Mon Sep 17 00:00:00 2001 From: abdarrahman abouzaid Date: Thu, 11 Jun 2020 15:21:11 +0200 Subject: [PATCH 11/96] clear errors when apply new filters --- app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js index fe33389eabad4..848ad60219a2b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js @@ -200,6 +200,7 @@ define([ * @returns {Filters} Chainable. */ apply: function () { + $('body').notification('clear'); this.set('applied', removeEmpty(this.filters)); return this; From 7f0f260d3110a055ca1f8fa4886e766dd303b6c1 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Mon, 6 Jul 2020 15:37:39 +0300 Subject: [PATCH 12/96] magento2/issues/12087: Changes for Widget class. --- app/code/Magento/Widget/Model/Widget.php | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index d07e84186b2c9..bb6c0f22fefdf 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -304,6 +304,8 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) if ($name == 'conditions') { $name = 'conditions_encoded'; $value = $this->conditionsHelper->encode($value); + } elseif ($this->isTextType($widget, $name)) { + $value = $this->encodeReservedChars($value); } elseif (is_array($value)) { $value = implode(',', $value); } elseif (trim($value) == '') { @@ -456,4 +458,41 @@ protected function sortParameters($firstElement, $secondElement) $bOrder = (int)$secondElement->getData('sort_order'); return $aOrder < $bOrder ? -1 : ($aOrder > $bOrder ? 1 : 0); } + + /** + * @param $string + * @return string|string[] + */ + private function encodeReservedChars($string) + { + $map = [ + '{' => urlencode('{'), + '}' => urlencode('}') + ]; + + return str_replace( + array_keys($map), + array_values($map), + $string + ); + } + + /** + * @param $widget + * @param $name + * @return bool + */ + private function isTextType($widget, $name) + { + $parameters = $widget->getParameters(); + + if (isset($parameters[$name]) && is_object($parameters[$name])) { + $type = $parameters[$name]->getType(); + if ($type == 'text') { + return true; + } + } + + return false; + } } From 2a48abee8a17331fdea9d3ba4738e5394d73a13b Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Mon, 6 Jul 2020 16:07:05 +0300 Subject: [PATCH 13/96] magento2/issues/12087: Changes for Adminhtml Widget Options class. --- .../Widget/Block/Adminhtml/Widget/Options.php | 4 ++++ app/code/Magento/Widget/Model/Widget.php | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php index 32bae10c801c8..87ae52b2eadda 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php @@ -158,6 +158,10 @@ protected function _addField($parameter) $data['value'] = $parameter->getValue(); } + if ($parameter->getType() == 'text' && $data['value'] != '') { + $data['value'] = $this->_widget->decodeReservedChars($data['value']); + } + //prepare unique id value if ($fieldName == 'unique_id' && $data['value'] == '') { $data['value'] = hash('sha256', microtime(1)); diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index bb6c0f22fefdf..12af534325224 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -477,6 +477,24 @@ private function encodeReservedChars($string) ); } + /** + * @param $string + * @return array + */ + public function decodeReservedChars($string) + { + $map = [ + '{' => urlencode('{'), + '}' => urlencode('}') + ]; + + return str_replace( + array_values($map), + array_keys($map), + $string + ); + } + /** * @param $widget * @param $name From cb0b3d407dc1ac5c42e202a515a2aee97574b57d Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Tue, 7 Jul 2020 11:01:36 +0300 Subject: [PATCH 14/96] magento2/issues/12087: Fix static test. --- app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php | 1 + app/code/Magento/Widget/Model/Widget.php | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php index 87ae52b2eadda..44c43055df8b9 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php @@ -136,6 +136,7 @@ public function addFields() * @return \Magento\Framework\Data\Form\Element\AbstractElement * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _addField($parameter) { diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 12af534325224..c01d8b226197d 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -293,6 +293,7 @@ public function getWidgetsArray($filters = []) * @param array $params Pre-configured Widget Params * @param bool $asIs Return result as widget directive(true) or as placeholder image(false) * @return string Widget directive ready to parse + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getWidgetDeclaration($type, $params = [], $asIs = true) { @@ -460,6 +461,8 @@ protected function sortParameters($firstElement, $secondElement) } /** + * Encode reserved chars + * * @param $string * @return string|string[] */ @@ -478,6 +481,8 @@ private function encodeReservedChars($string) } /** + * Decode reserved chars + * * @param $string * @return array */ @@ -496,6 +501,8 @@ public function decodeReservedChars($string) } /** + * Is text type Widget parameter + * * @param $widget * @param $name * @return bool From 1f84319d6905dc9cc862fd0cb1244489b44f5052 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Tue, 7 Jul 2020 14:39:30 +0300 Subject: [PATCH 15/96] magento2/issues/12087: Fix static test. --- app/code/Magento/Widget/Model/Widget.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index c01d8b226197d..49450ff6e017e 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -131,6 +131,7 @@ public function getWidgetByClassType($type) */ public function getConfigAsXml($type) { + // phpstan:ignore return $this->getXmlElementByType($type); } @@ -463,7 +464,7 @@ protected function sortParameters($firstElement, $secondElement) /** * Encode reserved chars * - * @param $string + * @param string $string * @return string|string[] */ private function encodeReservedChars($string) @@ -483,7 +484,7 @@ private function encodeReservedChars($string) /** * Decode reserved chars * - * @param $string + * @param string $string * @return array */ public function decodeReservedChars($string) @@ -503,8 +504,8 @@ public function decodeReservedChars($string) /** * Is text type Widget parameter * - * @param $widget - * @param $name + * @param \Magento\Framework\DataObject $widget + * @param string $name * @return bool */ private function isTextType($widget, $name) From da381030bdeca058a6979142fa1ac265628f9f1d Mon Sep 17 00:00:00 2001 From: engcom-Echo Date: Wed, 5 Aug 2020 18:28:52 +0300 Subject: [PATCH 16/96] added unit test --- app/code/Magento/Catalog/Helper/Image.php | 8 +++++++- app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index abb24b0768b2a..ab74b5694ce9f 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -384,7 +384,9 @@ public function backgroundColor($colorRGB) { // assume that 3 params were given instead of array if (!is_array($colorRGB)) { + //phpcs:disable $colorRGB = func_get_args(); + //phpcs:enabled } $this->_getModel()->setBackgroundColor($colorRGB); return $this; @@ -498,7 +500,11 @@ protected function initBaseFile() if ($this->getImageFile()) { $model->setBaseFile($this->getImageFile()); } else { - $model->setBaseFile($this->getProduct() ? $this->getProduct()->getData($model->getDestinationSubdir()) : ''); + $model->setBaseFile( + $this->getProduct() + ? $this->getProduct()->getData($model->getDestinationSubdir()) + : '' + ); } } return $this; diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index aa29972c91a62..c606b7537cc44 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -396,6 +396,14 @@ public function testGetWidth() $this->assertEquals($data['width'], $this->helper->getWidth()); } + /** + * Check initBaseFile without properties - product + */ + public function testGetUrlWithOutProduct() + { + $this->assertNull($this->helper->getUrl()); + } + /** * @param array $data * @dataProvider getHeightDataProvider From b6a66b33ca8acbd554765e2bcb5395d33dcc6510 Mon Sep 17 00:00:00 2001 From: yolouiese Date: Fri, 14 Aug 2020 20:53:42 +0800 Subject: [PATCH 17/96] magento/magento2#1724: Support batches processing for synchronization queue messages - updated synchronization consumer and added media content bulk service --- .../Model/Consume.php | 23 ++++++++++++++++--- .../Api/SynchronizeIdentitiesInterface.php | 23 +++++++++++++++++++ .../Model/Consume.php | 23 ++++++++++++++++--- 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index bcce3514e4ad9..d690225607da4 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -7,6 +7,8 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; /** @@ -19,19 +21,34 @@ class Consume */ private $synchronize; + /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + /** * @param SynchronizeInterface $synchronize + * @param SynchronizeIdentitiesInterface $synchronizeIdentities */ - public function __construct(SynchronizeInterface $synchronize) - { + public function __construct( + SynchronizeInterface $synchronize, + SynchronizeIdentitiesInterface $synchronizeIdentities + ) { $this->synchronize = $synchronize; + $this->synchronizeIdentities = $synchronizeIdentities; } /** * Run media files synchronization. + * @param string[] $message + * @throws LocalizedException */ - public function execute() : void + public function execute(array $message) : void { $this->synchronize->execute(); + + if (!empty($message)) { + $this->synchronizeIdentities->execute($message); + } } } diff --git a/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php new file mode 100644 index 0000000000000..7e21cbb570053 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php @@ -0,0 +1,23 @@ +synchronize = $synchronize; + $this->synchronizeFiles = $synchronizeFiles; } /** * Run media files synchronization. + * @param string[] $message + * @throws LocalizedException */ - public function execute() : void + public function execute(array $message) : void { $this->synchronize->execute(); + + if (!empty($message)) { + $this->synchronizeFiles->execute($message); + } } } From 29c3b265dfded16c3e688e505f15e1f055446731 Mon Sep 17 00:00:00 2001 From: yolouiese Date: Wed, 19 Aug 2020 02:13:48 +0800 Subject: [PATCH 18/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - implemented gallery synchronization functionality --- .../MediaContentSynchronization/Model/Consume.php | 13 +++++++------ .../MediaContentSynchronization/Model/Publish.php | 5 +++-- .../Magento/MediaContentSynchronization/etc/di.xml | 1 + .../MediaGallerySynchronization/Model/Consume.php | 12 ++++++------ .../MediaGallerySynchronization/Model/Publish.php | 5 +++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index d690225607da4..d91b426e4f3ee 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -8,6 +8,7 @@ namespace Magento\MediaContentSynchronization\Model; use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; @@ -40,15 +41,15 @@ public function __construct( /** * Run media files synchronization. - * @param string[] $message + * @param string[] $identities * @throws LocalizedException */ - public function execute(array $message) : void + public function execute(array $identities) : void { - $this->synchronize->execute(); - - if (!empty($message)) { - $this->synchronizeIdentities->execute($message); + if (!empty($identities)) { + $this->synchronizeIdentities->execute($identities); + } else { + $this->synchronize->execute(); } } } diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php index ad6fdd27d7067..f94328a3f4468 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -34,12 +34,13 @@ public function __construct(PublisherInterface $publisher) /** * Publish media content synchronization message to the message queue. + * @param array $contentIdentities */ - public function execute() : void + public function execute(array $contentIdentities = []) : void { $this->publisher->publish( self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, - [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION] + $contentIdentities ); } } diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index d4615c15206e5..e5347f1a11561 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -7,6 +7,7 @@ --> + diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php index c565e0f728bd1..981d60c1e8c28 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -40,15 +40,15 @@ public function __construct( /** * Run media files synchronization. - * @param string[] $message + * @param array $paths * @throws LocalizedException */ - public function execute(array $message) : void + public function execute(array $paths) : void { - $this->synchronize->execute(); - - if (!empty($message)) { - $this->synchronizeFiles->execute($message); + if (!empty($paths)) { + $this->synchronizeFiles->execute($paths); + } else { + $this->synchronize->execute(); } } } diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php index 386798d68d9df..a2db65911b1e4 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -34,12 +34,13 @@ public function __construct(PublisherInterface $publisher) /** * Publish media content synchronization message to the message queue. + * @param array $paths */ - public function execute() : void + public function execute(array $paths = []) : void { $this->publisher->publish( self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION, - [self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION] + $paths ); } } From bbb99e5a4b3bc82da0aa184ad3635e813f1b3d1c Mon Sep 17 00:00:00 2001 From: yolouiese Date: Fri, 21 Aug 2020 04:05:23 +0800 Subject: [PATCH 19/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - implemented media content sychronization --- .../Model/Consume.php | 18 +++++-- .../Model/Publish.php | 48 +++++++++++++++++-- .../etc/communication.xml | 2 +- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index d91b426e4f3ee..0136466eaed81 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -7,8 +7,9 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\Framework\Serialize\SerializerInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; @@ -17,6 +18,11 @@ */ class Consume { + /** + * @var SerializerInterface + */ + private $serializer; + /** * @var SynchronizeInterface */ @@ -28,24 +34,30 @@ class Consume private $synchronizeIdentities; /** + * @param SerializerInterface $serializer * @param SynchronizeInterface $synchronize * @param SynchronizeIdentitiesInterface $synchronizeIdentities */ public function __construct( + SerializerInterface $serializer, SynchronizeInterface $synchronize, SynchronizeIdentitiesInterface $synchronizeIdentities ) { + $this->serializer = $serializer; $this->synchronize = $synchronize; $this->synchronizeIdentities = $synchronizeIdentities; } /** * Run media files synchronization. - * @param string[] $identities + * @param OperationInterface $operation * @throws LocalizedException */ - public function execute(array $identities) : void + public function execute(OperationInterface $operation) : void { + $serializedData = $operation->getSerializedData(); + $identities = $this->serializer->unserialize($serializedData); + if (!empty($identities)) { $this->synchronizeIdentities->execute($identities); } else { diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php index f94328a3f4468..ee32df47215a9 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -7,7 +7,11 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\DataObject\IdentityGeneratorInterface; use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\Serialize\SerializerInterface; /** * Publish media content synchronization queue. @@ -19,16 +23,41 @@ class Publish */ private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var IdentityGeneratorInterface + */ + private $identityService; + /** * @var PublisherInterface */ private $publisher; /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param OperationInterfaceFactory $operationFactory + * @param IdentityGeneratorInterface $identityService * @param PublisherInterface $publisher + * @param SerializerInterface $serializer */ - public function __construct(PublisherInterface $publisher) - { + public function __construct( + OperationInterfaceFactory $operationFactory, + IdentityGeneratorInterface $identityService, + PublisherInterface $publisher, + SerializerInterface $serializer + ) { + $this->operationFactory = $operationFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; $this->publisher = $publisher; } @@ -38,9 +67,22 @@ public function __construct(PublisherInterface $publisher) */ public function execute(array $contentIdentities = []) : void { + $bulkUuid = $this->identityService->generateId(); + $dataToEncode = $this->serializer->serialize($contentIdentities); + + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, + 'serialized_data' => $dataToEncode, + 'status' => OperationInterface::STATUS_TYPE_OPEN, + ] + ]; + $operation = $this->operationFactory->create($data); + $this->publisher->publish( self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, - $contentIdentities + $operation ); } } diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml index e3436aee85331..05641b7432564 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/communication.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml @@ -7,7 +7,7 @@ --> - + From 2917c4b47bb37e55b01bd8850cc903c5e6fd76c7 Mon Sep 17 00:00:00 2001 From: yolouiese Date: Fri, 21 Aug 2020 07:26:33 +0800 Subject: [PATCH 20/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - added media content sychronizer and save contents to database --- .../Model/SynchronizeIdentities.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php new file mode 100644 index 0000000000000..42815adc7a18b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -0,0 +1,92 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->getEntityContents = $getEntityContents; + $this->fields = $fields; + } + + /** + * @inheritDoc + */ + public function execute(array $mediaContentIdentities): void + { + foreach ($mediaContentIdentities as $identity) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $identity[self::MEDIA_CONTENT_TYPE], + self::ENTITY_ID => $identity[self::MEDIA_CONTENT_ENTITY_ID], + self::FIELD => $identity[self::MEDIA_CONTENT_FIELD] + ] + ); + + if ($identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_PAGE + || $identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_BLOCK + ) { + $content = (string) $identity[self::MEDIA_CONTENT_FIELD]; + } else { + $content = implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)); + } + + $this->updateContentAssetLinks->execute( + $contentIdentity, + $content + ); + } + } +} From 16dc1fa89136018040542b03fde972180d445ebe Mon Sep 17 00:00:00 2001 From: yolouiese Date: Fri, 21 Aug 2020 19:47:03 +0800 Subject: [PATCH 21/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - added method to sync and save media content to database --- .../Model/SynchronizeIdentities.php | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php index 42815adc7a18b..5726bd9cf5f14 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -7,6 +7,7 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\Framework\App\ResourceConnection; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Model\GetEntityContentsInterface; @@ -17,12 +18,24 @@ class SynchronizeIdentities implements SynchronizeIdentitiesInterface private const ENTITY_TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; private const FIELD = 'field'; + private const MEDIA_CONTENT_TYPE = 'entity_type'; private const MEDIA_CONTENT_ENTITY_ID = 'entity_id'; private const MEDIA_CONTENT_FIELD = 'field'; + private const FIELD_CMS_PAGE = 'cms_page'; private const FIELD_CMS_BLOCK = 'cms_block'; + private const ID_CMS_PAGE = 'page_id'; + private const ID_CMS_BLOCK = 'block_id'; + + private const COLUMN_CMS_CONTENT = 'content'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + /** * @var ContentIdentityInterfaceFactory */ @@ -39,26 +52,21 @@ class SynchronizeIdentities implements SynchronizeIdentitiesInterface private $getEntityContents; /** - * @var array - */ - private $fields; - - /** + * @param ResourceConnection $resourceConnection * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param UpdateContentAssetLinksInterface $updateContentAssetLinks * @param GetEntityContentsInterface $getEntityContents - * @param array $fields */ public function __construct( + ResourceConnection $resourceConnection, ContentIdentityInterfaceFactory $contentIdentityFactory, UpdateContentAssetLinksInterface $updateContentAssetLinks, - GetEntityContentsInterface $getEntityContents, - array $fields = [] + GetEntityContentsInterface $getEntityContents ) { + $this->resourceConnection = $resourceConnection; $this->contentIdentityFactory = $contentIdentityFactory; $this->updateContentAssetLinks = $updateContentAssetLinks; $this->getEntityContents = $getEntityContents; - $this->fields = $fields; } /** @@ -78,7 +86,7 @@ public function execute(array $mediaContentIdentities): void if ($identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_PAGE || $identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_BLOCK ) { - $content = (string) $identity[self::MEDIA_CONTENT_FIELD]; + $content = $this->getCmsMediaContent($identity[self::MEDIA_CONTENT_TYPE], $identity[self::MEDIA_CONTENT_ENTITY_ID]); } else { $content = implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)); } @@ -89,4 +97,25 @@ public function execute(array $mediaContentIdentities): void ); } } + + /** + * Get cms media content from database + * + * @param string $tableName + * @param string $cmsId + * @return string + */ + private function getCmsMediaContent(string $tableName, string $cmsId): string + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName($tableName); + $idField = $tableName == self::FIELD_CMS_BLOCK ? $idField = self::ID_CMS_BLOCK : self::ID_CMS_PAGE; + + $select = $connection->select() + ->from($tableName, self::COLUMN_CMS_CONTENT) + ->where($idField . '= ?', $cmsId); + $data = $connection->fetchOne($select); + + return (string)$data; + } } From 771dc31f5ef1236db3ac54c3062f663cfee7d8b0 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych Date: Fri, 21 Aug 2020 17:13:19 +0300 Subject: [PATCH 22/96] Introduce granulated Media Gallery ACL resources and enforce for old media gallery --- .../Adminhtml/Wysiwyg/Images/DeleteFiles.php | 5 ++++ .../Adminhtml/Wysiwyg/Images/DeleteFolder.php | 5 ++++ .../Adminhtml/Wysiwyg/Images/NewFolder.php | 7 ++++- .../Adminhtml/Wysiwyg/Images/OnInsert.php | 5 ++++ .../Adminhtml/Wysiwyg/Images/Upload.php | 7 ++++- .../Adminhtml/Directories/Create.php | 2 +- .../Adminhtml/Directories/Delete.php | 2 +- .../Controller/Adminhtml/Image/Delete.php | 2 +- .../Controller/Adminhtml/Image/Upload.php | 2 +- app/code/Magento/MediaGalleryUi/etc/acl.xml | 26 +++++++++++++++++++ 10 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/etc/acl.xml diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php index fa873930aaade..848a10950e0ba 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php @@ -13,6 +13,11 @@ */ class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::delete_assets'; + /** * @var \Magento\Framework\Controller\Result\JsonFactory */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index 29f84e0b2e534..8a67d9220e11c 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -17,6 +17,11 @@ */ class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::delete_folder'; + /** * @var \Magento\Framework\Controller\Result\JsonFactory */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index 82d200beb6dc9..de462f82c8911 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -14,6 +14,11 @@ */ class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::create_folder'; + /** * @var \Magento\Framework\Controller\Result\JsonFactory */ @@ -65,7 +70,7 @@ public function execute() } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php index 3244a7d14f0a3..516742ffd2ee8 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php @@ -8,6 +8,11 @@ class OnInsert extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::insert_assets'; + /** * @var \Magento\Framework\Controller\Result\RawFactory */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 9bad371aa84d7..6e48dfe7c5f10 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -17,6 +17,11 @@ */ class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::upload_assets'; + /** * @var \Magento\Framework\Controller\Result\JsonFactory */ @@ -74,7 +79,7 @@ public function execute() } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - + return $resultJson->setData($response); } } diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php index 3d4af88e4ad67..147fc48910113 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::create_folder'; /** * @var CreateDirectoriesByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php index 56f12c5139d65..1ca2512826504 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::delete_folder'; /** * @var DeleteAssetsByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php index a5d1cee7abf41..d630d93688d28 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::delete_assets'; /** * @var DeleteImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php index e965d94b33f0c..0d3f9a78ae3ca 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::upload_assets'; /** * @var UploadImage diff --git a/app/code/Magento/MediaGalleryUi/etc/acl.xml b/app/code/Magento/MediaGalleryUi/etc/acl.xml new file mode 100644 index 0000000000000..7c8784e412814 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/acl.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + From d5276dd5b66d6c8f8e50d70e91ea515322b1af7b Mon Sep 17 00:00:00 2001 From: yolouiese Date: Fri, 21 Aug 2020 23:37:06 +0800 Subject: [PATCH 23/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - fixed failed static test --- .../Magento/MediaContentSynchronization/Model/Consume.php | 1 + .../Magento/MediaContentSynchronization/Model/Publish.php | 3 ++- .../Model/SynchronizeIdentities.php | 4 +++- app/code/Magento/MediaContentSynchronization/composer.json | 3 ++- app/code/Magento/MediaContentSynchronizationApi/composer.json | 3 ++- .../Magento/MediaGallerySynchronization/Model/Consume.php | 1 + .../Magento/MediaGallerySynchronization/Model/Publish.php | 3 ++- 7 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index 0136466eaed81..da18d733ff5ab 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -50,6 +50,7 @@ public function __construct( /** * Run media files synchronization. + * * @param OperationInterface $operation * @throws LocalizedException */ diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php index ee32df47215a9..5b7d0768955fc 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -62,7 +62,8 @@ public function __construct( } /** - * Publish media content synchronization message to the message queue. + * Publish media content synchronization message to the message queue + * * @param array $contentIdentities */ public function execute(array $contentIdentities = []) : void diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php index 5726bd9cf5f14..ad72ec34a4be6 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -86,7 +86,9 @@ public function execute(array $mediaContentIdentities): void if ($identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_PAGE || $identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_BLOCK ) { - $content = $this->getCmsMediaContent($identity[self::MEDIA_CONTENT_TYPE], $identity[self::MEDIA_CONTENT_ENTITY_ID]); + $content = $this->getCmsMediaContent( + $identity[self::MEDIA_CONTENT_TYPE], $identity[self::MEDIA_CONTENT_ENTITY_ID] + ); } else { $content = implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)); } diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index 3be5f535487ec..5e46ef371c204 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -6,7 +6,8 @@ "magento/framework": "*", "magento/module-media-content-synchronization-api": "*", "magento/framework-message-queue": "*", - "magento/module-media-content-api": "*" + "magento/module-media-content-api": "*", + "magento/module-asynchronous-operations": "*" }, "suggest": { "magento/module-media-gallery-synchronization": "*" diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json index 1f1e5e4b51c5b..398aaf1de8071 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/composer.json +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -3,7 +3,8 @@ "description": "Magento module responsible for the media content synchronization implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-media-content-api": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php index 981d60c1e8c28..b796d4225d08c 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -40,6 +40,7 @@ public function __construct( /** * Run media files synchronization. + * * @param array $paths * @throws LocalizedException */ diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php index a2db65911b1e4..ec314416e36ee 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -33,7 +33,8 @@ public function __construct(PublisherInterface $publisher) } /** - * Publish media content synchronization message to the message queue. + * Publish media content synchronization message to the message queue + * * @param array $paths */ public function execute(array $paths = []) : void From d9e9075859fdda71bc349f114f2e99b79e5137ae Mon Sep 17 00:00:00 2001 From: yolouiese Date: Sat, 22 Aug 2020 01:40:36 +0800 Subject: [PATCH 24/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - fixed failed static test --- .../Model/SynchronizeIdentities.php | 3 ++- app/code/Magento/MediaContentSynchronization/composer.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php index ad72ec34a4be6..2715d5e49fd6b 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -87,7 +87,8 @@ public function execute(array $mediaContentIdentities): void || $identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_BLOCK ) { $content = $this->getCmsMediaContent( - $identity[self::MEDIA_CONTENT_TYPE], $identity[self::MEDIA_CONTENT_ENTITY_ID] + $identity[self::MEDIA_CONTENT_TYPE], + $identity[self::MEDIA_CONTENT_ENTITY_ID] ); } else { $content = implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)); diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index 5e46ef371c204..f435f311deb46 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -4,6 +4,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/framework-bulk": "*", "magento/module-media-content-synchronization-api": "*", "magento/framework-message-queue": "*", "magento/module-media-content-api": "*", From b8e89a1d484fc0af009b57441910ada5a110b471 Mon Sep 17 00:00:00 2001 From: yolouiese Date: Thu, 27 Aug 2020 02:19:17 +0800 Subject: [PATCH 25/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - fixed failed static test and added integration test --- .../Model/SynchronizeIdentities.php | 22 +-- .../Model/SynchronizeIdentitiesTest.php | 161 ++++++++++++++++++ .../MediaContentSynchronization/composer.json | 1 - 3 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php index 2715d5e49fd6b..cc91b7b932483 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -19,10 +19,6 @@ class SynchronizeIdentities implements SynchronizeIdentitiesInterface private const ENTITY_ID = 'entityId'; private const FIELD = 'field'; - private const MEDIA_CONTENT_TYPE = 'entity_type'; - private const MEDIA_CONTENT_ENTITY_ID = 'entity_id'; - private const MEDIA_CONTENT_FIELD = 'field'; - private const FIELD_CMS_PAGE = 'cms_page'; private const FIELD_CMS_BLOCK = 'cms_block'; @@ -77,18 +73,18 @@ public function execute(array $mediaContentIdentities): void foreach ($mediaContentIdentities as $identity) { $contentIdentity = $this->contentIdentityFactory->create( [ - self::ENTITY_TYPE => $identity[self::MEDIA_CONTENT_TYPE], - self::ENTITY_ID => $identity[self::MEDIA_CONTENT_ENTITY_ID], - self::FIELD => $identity[self::MEDIA_CONTENT_FIELD] + self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], + self::ENTITY_ID => $identity[self::ENTITY_ID], + self::FIELD => $identity[self::FIELD] ] ); - if ($identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_PAGE - || $identity[self::MEDIA_CONTENT_TYPE] === self::FIELD_CMS_BLOCK + if ($identity[self::ENTITY_TYPE] === self::FIELD_CMS_PAGE + || $identity[self::ENTITY_TYPE] === self::FIELD_CMS_BLOCK ) { $content = $this->getCmsMediaContent( - $identity[self::MEDIA_CONTENT_TYPE], - $identity[self::MEDIA_CONTENT_ENTITY_ID] + $identity[self::ENTITY_TYPE], + $identity[self::ENTITY_ID] ); } else { $content = implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)); @@ -105,10 +101,10 @@ public function execute(array $mediaContentIdentities): void * Get cms media content from database * * @param string $tableName - * @param string $cmsId + * @param int $cmsId * @return string */ - private function getCmsMediaContent(string $tableName, string $cmsId): string + private function getCmsMediaContent(string $tableName, int $cmsId): string { $connection = $this->resourceConnection->getConnection(); $tableName = $this->resourceConnection->getTableName($tableName); diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php new file mode 100644 index 0000000000000..eb21723db2c17 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php @@ -0,0 +1,161 @@ +contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); + $this->synchronize = Bootstrap::getObjectManager()->get(SynchronizeInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param array $mediaContentIdentities + * @throws IntegrationException + */ + public function testExecute(array $mediaContentIdentities): void + { + $this->assertNotEmpty($mediaContentIdentities); + $this->synchronizeIdentities->execute($mediaContentIdentities); + + foreach ($mediaContentIdentities as $contentIdentity) { + $assetId = 2020; + $categoryId = 28767; + $productId = 1567; + $pageId = 5; + $blockId = 1; + $identity = $this->contentIdentityFactory->create($contentIdentity); + $this->assertEquals([$assetId], $this->getAssetIds->execute($identity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(4, count($synchronizedContentIdentities)); + $this->assertEquals($categoryId, $synchronizedContentIdentities[0]->getEntityId()); + $this->assertEquals($productId, $synchronizedContentIdentities[1]->getEntityId()); + $this->assertEquals($pageId, $synchronizedContentIdentities[2]->getEntityId()); + $this->assertEquals($blockId, $synchronizedContentIdentities[3]->getEntityId()); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + $this->getCategoryIdentities(), + $this->getProductIdentities(), + $this->getCmsPageIdentities(), + $this->getCmsBlockIdentities() + ] + ] + ]; + } + + /** + * Format category media content identities + */ + public function getCategoryIdentities() + { + $categoryId = 28767; + return $contentIdentity = [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => $categoryId + ]; + } + + /** + * Format product media content identities + */ + public function getProductIdentities() + { + $productId = 1567; + return $contentIdentity = [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => $productId + ]; + } + + /** + * Format cms page media content identities + */ + public function getCmsPageIdentities() + { + $pageId = 5; + return $contentIdentity = [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ]; + } + + /** + * Format cms block media content identities + */ + public function getCmsBlockIdentities() + { + $blockId = 1; + return $contentIdentity = [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ]; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index f435f311deb46..9f0f4f9588ad6 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -6,7 +6,6 @@ "magento/framework": "*", "magento/framework-bulk": "*", "magento/module-media-content-synchronization-api": "*", - "magento/framework-message-queue": "*", "magento/module-media-content-api": "*", "magento/module-asynchronous-operations": "*" }, From 9fac5f7173db77e933e5a59e671151d9bf0b62c2 Mon Sep 17 00:00:00 2001 From: abdarrahman abouzaid Date: Thu, 27 Aug 2020 00:56:28 +0200 Subject: [PATCH 26/96] provide mftf test --- ...erRemoveErrorMessageBeforeApplyFilters.xml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml new file mode 100644 index 0000000000000..3e4bac32d38e9 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml @@ -0,0 +1,95 @@ + + + + + + + + + <description value="Test log in to uI and Remove Error Message Before Apply Filters"/> + <testCaseId value="MC-142721"/> + <severity value="CRITICAL"/> + <group value="uI"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="defaultSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="rootCategory" /> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="rootCategory" /> + </createData> + <!-- <createData entity="_defaultStore" stepKey="defaultStore"/> --> + + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteProduct2" createDataKey="createProduct2"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Filter created simple product in grid and add category and website created in create data--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowOfCreatedSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <actionGroup ref="AddWebsiteToProductActionGroup" stepKey="updateSimpleProductAddingWebsiteCreated"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Search updated simple product(from above step) in the grid by StoreView and Name--> + <actionGroup ref="FilterProductInGridByStoreViewAndNameActionGroup" stepKey="searchCreatedSimpleProductInGrid"> + <argument name="storeView" value="{{customStoreEN.name}}"/> + <argument name="productName" value="$$createProduct2.name$$"/> + </actionGroup> + + <!--Go to stores and delete website created in create data--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Go to grid page and verify AssertErrorMessage--> + <actionGroup ref="AssertErrorMessageAfterDeletingWebsiteActionGroup" stepKey="verifyErrorMessage"> + <argument name="errorMessage" value="Something went wrong with processing the default view and we have restored the filter to its original state."/> + </actionGroup> + + <!--Apply new filters to verify error message is removed --> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.storeViewDropdown('Default Store View')}}" stepKey="clickStoreViewDropdown"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="$$createProduct.name$$" stepKey="fillProductNameInNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <see selector="{{AdminProductGridFilterSection.nthRow('1')}}" userInput="$$createProduct.name$$" stepKey="seeFirstRowToVerifyProductVisibleInGrid"/> + <dontSeeElement selector="{{AdminMessagesSection.error}}" stepKey="dontSeeErrorMessage"/> + + </test> +</tests> From 3852c89cebccfef1da7242d73be40e4d831d3a97 Mon Sep 17 00:00:00 2001 From: abdarrahman abouzaid <abdarrahman.abouzaid@gmail.com> Date: Thu, 27 Aug 2020 01:05:10 +0200 Subject: [PATCH 27/96] update test file --- ...GridFilterRemoveErrorMessageBeforeApplyFilters.xml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml index 3e4bac32d38e9..c931e4f2b54c7 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml @@ -1,11 +1,13 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ + * Copyright (©) 2020 Pinpoint Designs LTD. All right's reserved. + * + * Author: abdarrahman.abouzaid@gmail.com + * Website: http://www.pinpointdesigns.co.uk + */ --> -<!-- Test XML Example --> +<!-- Test XML #28687 PR --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminGridFilterRemoveErrorMessageBeforeApplyFilters"> <annotations> @@ -28,7 +30,6 @@ <createData entity="defaultSimpleProduct" stepKey="createProduct2"> <requiredEntity createDataKey="rootCategory" /> </createData> - <!-- <createData entity="_defaultStore" stepKey="defaultStore"/> --> <!--Create website--> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> From 561c84944573aa9a6af120d198bdd95b04d62a1e Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Thu, 27 Aug 2020 16:03:05 +0800 Subject: [PATCH 28/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated integration test --- .../Model/SynchronizeIdentitiesTest.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php index eb21723db2c17..9660d81b34d77 100644 --- a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php @@ -72,19 +72,17 @@ public function testExecute(array $mediaContentIdentities): void foreach ($mediaContentIdentities as $contentIdentity) { $assetId = 2020; - $categoryId = 28767; - $productId = 1567; - $pageId = 5; - $blockId = 1; $identity = $this->contentIdentityFactory->create($contentIdentity); $this->assertEquals([$assetId], $this->getAssetIds->execute($identity)); $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); $this->assertEquals(4, count($synchronizedContentIdentities)); - $this->assertEquals($categoryId, $synchronizedContentIdentities[0]->getEntityId()); - $this->assertEquals($productId, $synchronizedContentIdentities[1]->getEntityId()); - $this->assertEquals($pageId, $synchronizedContentIdentities[2]->getEntityId()); - $this->assertEquals($blockId, $synchronizedContentIdentities[3]->getEntityId()); + + $syncedIds = []; + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $syncedIds[] = (int)$syncedContentIdentity->getEntityId(); + } + $this->assertContains($contentIdentity['entityId'], $syncedIds); } } From a36a924e63e55dbdd0423aa4ac9d7dee87053334 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Thu, 27 Aug 2020 20:36:23 +0800 Subject: [PATCH 29/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - fixed failed integration test --- .../Test/Integration/Model/SynchronizeIdentitiesTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php index 9660d81b34d77..4e7debf11d6b7 100644 --- a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php @@ -111,7 +111,7 @@ public function filesProvider(): array public function getCategoryIdentities() { $categoryId = 28767; - return $contentIdentity = [ + return [ 'entityType' => 'catalog_category', 'field' => 'description', 'entityId' => $categoryId @@ -124,7 +124,7 @@ public function getCategoryIdentities() public function getProductIdentities() { $productId = 1567; - return $contentIdentity = [ + return [ 'entityType' => 'catalog_product', 'field' => 'description', 'entityId' => $productId @@ -137,7 +137,7 @@ public function getProductIdentities() public function getCmsPageIdentities() { $pageId = 5; - return $contentIdentity = [ + return [ 'entityType' => 'cms_page', 'field' => 'content', 'entityId' => $pageId @@ -150,7 +150,7 @@ public function getCmsPageIdentities() public function getCmsBlockIdentities() { $blockId = 1; - return $contentIdentity = [ + return [ 'entityType' => 'cms_block', 'field' => 'content', 'entityId' => $blockId From 7517b21aa87f9e47aefa1ef0986f0198e6f4d70b Mon Sep 17 00:00:00 2001 From: abdarrahman abouzaid <abdarrahman.abouzaid@gmail.com> Date: Thu, 27 Aug 2020 19:24:07 +0200 Subject: [PATCH 30/96] provide PR requested changes --- ...terRemoveErrorMessageBeforeApplyFilters.xml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml index c931e4f2b54c7..09c87010b5743 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml @@ -1,27 +1,22 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- /** - * Copyright (©) 2020 Pinpoint Designs LTD. All right's reserved. - * - * Author: abdarrahman.abouzaid@gmail.com - * Website: http://www.pinpointdesigns.co.uk - */ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ --> -<!-- Test XML #28687 PR --> + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminGridFilterRemoveErrorMessageBeforeApplyFilters"> <annotations> <stories value="Reset Error Messages"/> <title value="Remove Error Message Before Apply Filters"/> - <description value="Test log in to uI and Remove Error Message Before Apply Filters"/> - <testCaseId value="MC-142721"/> + <description value="Test login to Admin UI and Remove Error Message Before Apply Filters"/> <severity value="CRITICAL"/> - <group value="uI"/> - <group value="mtf_migrated"/> + <group value="ui"/> </annotations> <before> - <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="NewRootCategory" stepKey="rootCategory"/> <createData entity="defaultSimpleProduct" stepKey="createProduct"> @@ -49,7 +44,6 @@ </actionGroup> </before> <after> - <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteProduct2" createDataKey="createProduct2"/> From 6a9f506261c85cc49c71a78c338e3c2dbf3e21af Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Fri, 28 Aug 2020 04:05:38 +0800 Subject: [PATCH 31/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated pull request with requested changes --- .../Model/Consume.php | 29 +++- .../Model/Publish.php | 7 +- .../MediaContentSynchronization/etc/di.xml | 1 - .../SynchronizeIdentitiesCatalog.php | 50 +++++++ .../SynchronizeIdentitiesCatalogTest.php} | 108 ++++++--------- .../SynchronizeIdentitiesCms.php} | 44 +----- .../SynchronizeIdentitiesCmsTest.php | 125 ++++++++++++++++++ 7 files changed, 246 insertions(+), 118 deletions(-) create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php rename app/code/Magento/{MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php => MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php} (54%) rename app/code/Magento/{MediaContentSynchronization/Model/SynchronizeIdentities.php => MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php} (62%) create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index da18d733ff5ab..6aa8e6e831a25 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -10,6 +10,7 @@ use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\SerializerInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; @@ -18,11 +19,20 @@ */ class Consume { + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + /** * @var SerializerInterface */ private $serializer; + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + /** * @var SynchronizeInterface */ @@ -35,15 +45,18 @@ class Consume /** * @param SerializerInterface $serializer + * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param SynchronizeInterface $synchronize * @param SynchronizeIdentitiesInterface $synchronizeIdentities */ public function __construct( SerializerInterface $serializer, + ContentIdentityInterfaceFactory $contentIdentityFactory, SynchronizeInterface $synchronize, SynchronizeIdentitiesInterface $synchronizeIdentities ) { $this->serializer = $serializer; + $this->contentIdentityFactory = $contentIdentityFactory; $this->synchronize = $synchronize; $this->synchronizeIdentities = $synchronizeIdentities; } @@ -56,11 +69,19 @@ public function __construct( */ public function execute(OperationInterface $operation) : void { - $serializedData = $operation->getSerializedData(); - $identities = $this->serializer->unserialize($serializedData); - + $identities = $this->serializer->unserialize($operation->getSerializedData()); if (!empty($identities)) { - $this->synchronizeIdentities->execute($identities); + $contentIdentity = []; + foreach ($identities as $identity) { + $contentIdentity[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], + self::ENTITY_ID => $identity[self::ENTITY_ID], + self::FIELD => $identity[self::FIELD] + ] + ); + } + $this->synchronizeIdentities->execute($contentIdentity); } else { $this->synchronize->execute(); } diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php index 5b7d0768955fc..d9e89fea7d4d2 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -68,14 +68,11 @@ public function __construct( */ public function execute(array $contentIdentities = []) : void { - $bulkUuid = $this->identityService->generateId(); - $dataToEncode = $this->serializer->serialize($contentIdentities); - $data = [ 'data' => [ - 'bulk_uuid' => $bulkUuid, + 'bulk_uuid' => $this->identityService->generateId(), 'topic_name' => self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, - 'serialized_data' => $dataToEncode, + 'serialized_data' => $this->serializer->serialize($contentIdentities), 'status' => OperationInterface::STATUS_TYPE_OPEN, ] ]; diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index e5347f1a11561..d4615c15206e5 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -7,7 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaContentSynchronization\Model\Synchronize"/> - <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface" type="Magento\MediaContentSynchronization\Model\SynchronizeIdentities"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php new file mode 100644 index 0000000000000..068d733e14605 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Model\Synchronizer; + +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentitiesCatalog implements SynchronizeIdentitiesInterface +{ + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param GetEntityContentsInterface $getEntityContents + */ + public function __construct( + UpdateContentAssetLinksInterface $updateContentAssetLinks, + GetEntityContentsInterface $getEntityContents + ) { + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->getEntityContents = $getEntityContents; + } + + /** + * @inheritDoc + */ + public function execute(array $mediaContentIdentities): void + { + foreach ($mediaContentIdentities as $identity) { + $this->updateContentAssetLinks->execute( + $identity, + implode(PHP_EOL, $this->getEntityContents->execute($identity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php similarity index 54% rename from app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php rename to app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php index 4e7debf11d6b7..ae4bb77881834 100644 --- a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/SynchronizeIdentitiesTest.php +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php @@ -5,9 +5,10 @@ */ declare(strict_types=1); -namespace Magento\MediaContentSynchronization\Test\Integration\Model; +namespace Magento\MediaContentSynchronizationCatalog\Test\Integration\Model\Synchronizer; use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; @@ -17,10 +18,14 @@ use PHPUnit\Framework\TestCase; /** - * Test for SynchronizeIdentities. + * Test for catalog SynchronizeIdentities. */ -class SynchronizeIdentitiesTest extends TestCase +class SynchronizeIdentitiesCatalogTest extends TestCase { + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + /** * @var ContentIdentityInterfaceFactory */ @@ -59,30 +64,37 @@ protected function setUp(): void * @dataProvider filesProvider * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php - * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php - * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php - * @param array $mediaContentIdentities + * @param ContentIdentityInterface[] $mediaContentIdentities * @throws IntegrationException */ public function testExecute(array $mediaContentIdentities): void { - $this->assertNotEmpty($mediaContentIdentities); - $this->synchronizeIdentities->execute($mediaContentIdentities); + $contentIdentities = []; + foreach ($mediaContentIdentities as $mediaContentIdentity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE], + self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID], + self::FIELD => $mediaContentIdentity[self::FIELD] + ] + ); + } - foreach ($mediaContentIdentities as $contentIdentity) { - $assetId = 2020; - $identity = $this->contentIdentityFactory->create($contentIdentity); - $this->assertEquals([$assetId], $this->getAssetIds->execute($identity)); + $this->assertNotEmpty($contentIdentities); + $this->synchronizeIdentities->execute($contentIdentities); + foreach ($contentIdentities as $contentIdentity) { + $assetId = 2020; + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); - $this->assertEquals(4, count($synchronizedContentIdentities)); + $this->assertEquals(2, count($synchronizedContentIdentities)); $syncedIds = []; foreach ($synchronizedContentIdentities as $syncedContentIdentity) { - $syncedIds[] = (int)$syncedContentIdentity->getEntityId(); + $syncedIds[] = $syncedContentIdentity->getEntityId(); } - $this->assertContains($contentIdentity['entityId'], $syncedIds); + $this->assertContains($contentIdentity->getEntityId(), $syncedIds); } } @@ -96,64 +108,18 @@ public function filesProvider(): array return [ [ [ - $this->getCategoryIdentities(), - $this->getProductIdentities(), - $this->getCmsPageIdentities(), - $this->getCmsBlockIdentities() + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => 28767 + ], + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => 1567 + ] ] ] ]; } - - /** - * Format category media content identities - */ - public function getCategoryIdentities() - { - $categoryId = 28767; - return [ - 'entityType' => 'catalog_category', - 'field' => 'description', - 'entityId' => $categoryId - ]; - } - - /** - * Format product media content identities - */ - public function getProductIdentities() - { - $productId = 1567; - return [ - 'entityType' => 'catalog_product', - 'field' => 'description', - 'entityId' => $productId - ]; - } - - /** - * Format cms page media content identities - */ - public function getCmsPageIdentities() - { - $pageId = 5; - return [ - 'entityType' => 'cms_page', - 'field' => 'content', - 'entityId' => $pageId - ]; - } - - /** - * Format cms block media content identities - */ - public function getCmsBlockIdentities() - { - $blockId = 1; - return [ - 'entityType' => 'cms_block', - 'field' => 'content', - 'entityId' => $blockId - ]; - } } diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php similarity index 62% rename from app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php rename to app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php index cc91b7b932483..5685e284e20c3 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php @@ -5,26 +5,19 @@ */ declare(strict_types=1); -namespace Magento\MediaContentSynchronization\Model; +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; use Magento\Framework\App\ResourceConnection; -use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -class SynchronizeIdentities implements SynchronizeIdentitiesInterface +class SynchronizeIdentitiesCms implements SynchronizeIdentitiesInterface { - private const ENTITY_TYPE = 'entityType'; - private const ENTITY_ID = 'entityId'; - private const FIELD = 'field'; - private const FIELD_CMS_PAGE = 'cms_page'; private const FIELD_CMS_BLOCK = 'cms_block'; - private const ID_CMS_PAGE = 'page_id'; private const ID_CMS_BLOCK = 'block_id'; - private const COLUMN_CMS_CONTENT = 'content'; /** @@ -32,11 +25,6 @@ class SynchronizeIdentities implements SynchronizeIdentitiesInterface */ private $resourceConnection; - /** - * @var ContentIdentityInterfaceFactory - */ - private $contentIdentityFactory; - /** * @var UpdateContentAssetLinksInterface */ @@ -49,18 +37,15 @@ class SynchronizeIdentities implements SynchronizeIdentitiesInterface /** * @param ResourceConnection $resourceConnection - * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param UpdateContentAssetLinksInterface $updateContentAssetLinks * @param GetEntityContentsInterface $getEntityContents */ public function __construct( ResourceConnection $resourceConnection, - ContentIdentityInterfaceFactory $contentIdentityFactory, UpdateContentAssetLinksInterface $updateContentAssetLinks, GetEntityContentsInterface $getEntityContents ) { $this->resourceConnection = $resourceConnection; - $this->contentIdentityFactory = $contentIdentityFactory; $this->updateContentAssetLinks = $updateContentAssetLinks; $this->getEntityContents = $getEntityContents; } @@ -71,29 +56,14 @@ public function __construct( public function execute(array $mediaContentIdentities): void { foreach ($mediaContentIdentities as $identity) { - $contentIdentity = $this->contentIdentityFactory->create( - [ - self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], - self::ENTITY_ID => $identity[self::ENTITY_ID], - self::FIELD => $identity[self::FIELD] - ] - ); - - if ($identity[self::ENTITY_TYPE] === self::FIELD_CMS_PAGE - || $identity[self::ENTITY_TYPE] === self::FIELD_CMS_BLOCK + if ($identity->getEntityType() === self::FIELD_CMS_PAGE + || $identity->getEntityType() === self::FIELD_CMS_BLOCK ) { - $content = $this->getCmsMediaContent( - $identity[self::ENTITY_TYPE], - $identity[self::ENTITY_ID] + $this->updateContentAssetLinks->execute( + $identity, + $this->getCmsMediaContent($identity->getEntityType(), (int)$identity->getEntityId()) ); - } else { - $content = implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)); } - - $this->updateContentAssetLinks->execute( - $contentIdentity, - $content - ); } } diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php new file mode 100644 index 0000000000000..48841ecf24658 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Test\Integration\Model\Synchronizer; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for CMS SynchronizeIdentities. + */ +class SynchronizeIdentitiesCmsTest extends TestCase +{ + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + /** + * @var SynchronizeInterface + */ + private $synchronize; + + protected function setUp(): void + { + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); + $this->synchronize = Bootstrap::getObjectManager()->get(SynchronizeInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param ContentIdentityInterface[] $mediaContentIdentities + * @throws IntegrationException + */ + public function testExecute(array $mediaContentIdentities): void + { + $contentIdentities = []; + foreach ($mediaContentIdentities as $mediaContentIdentity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE], + self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID], + self::FIELD => $mediaContentIdentity[self::FIELD] + ] + ); + } + + $this->assertNotEmpty($contentIdentities); + $this->synchronizeIdentities->execute($contentIdentities); + + foreach ($contentIdentities as $contentIdentity) { + $assetId = 2020; + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + $syncedIds = []; + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $syncedIds[] = $syncedContentIdentity->getEntityId(); + } + $this->assertContains($contentIdentity->getEntityId(), $syncedIds); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => 5 + ], + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => 1 + ] + ] + ] + ]; + } +} From bce263f1d8fe5efc7ca8aaa8162cc17a3e1a17b6 Mon Sep 17 00:00:00 2001 From: SmVladyslav <vlatame.tsg@gmail.com> Date: Fri, 28 Aug 2020 13:06:03 +0300 Subject: [PATCH 32/96] MC-37117: Unexpected loading of the "New Review" page in the Admin-panel --- app/code/Magento/Review/Block/Adminhtml/Add.php | 9 ++------- .../Magento/Review/Block/Adminhtml/Add/Form.php | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 2edd76879d8dc..5f739b2595418 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -27,15 +27,10 @@ protected function _construct() $this->_mode = 'add'; $this->buttonList->update('save', 'label', __('Save Review')); $this->buttonList->update('save', 'id', 'save_button'); + $this->buttonList->update('save', 'style', 'display: none;'); $this->buttonList->update('reset', 'id', 'reset_button'); + $this->buttonList->update('reset', 'style', 'display: none;'); $this->buttonList->update('reset', 'onclick', 'window.review.formReset()'); - $this->_formScripts[] = ' - require(["prototype"], function(){ - toggleParentVis("add_review_form"); - toggleVis("save_button"); - toggleVis("reset_button"); - }); - '; // @codingStandardsIgnoreStart $this->_formInitScripts[] = ' require(["jquery","Magento_Review/js/rating","prototype"], function(jQuery, rating){ diff --git a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php index 04e6343eb43ca..efffa7a02678a 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Review\Block\Adminhtml\Add; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml add product review form * @@ -26,6 +29,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_systemStore; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -33,6 +41,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Store\Model\System\Store $systemStore * @param \Magento\Review\Helper\Data $reviewData * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -40,10 +49,12 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Store\Model\System\Store $systemStore, \Magento\Review\Helper\Data $reviewData, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null ) { $this->_reviewData = $reviewData; $this->_systemStore = $systemStore; + $this->secureRenderer = $htmlRenderer ?: ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -59,6 +70,8 @@ protected function _prepareForm() $form = $this->_formFactory->create(); $fieldset = $form->addFieldset('add_review_form', ['legend' => __('Review Details')]); + $beforeHtml = $this->secureRenderer->renderStyleAsTag('display: none;', '#edit_form'); + $fieldset->setBeforeElementHtml($beforeHtml); $fieldset->addField('product_name', 'note', ['label' => __('Product'), 'text' => 'product_name']); From 2fb949e4cc78f3c7fa10fce5a177ec2ce9dd4bc4 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Fri, 28 Aug 2020 18:49:12 +0800 Subject: [PATCH 33/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated pull request with requested changes --- .../Model/SynchronizeIdentities.php | 109 ++++++++++++++++++ .../MediaContentSynchronization/etc/di.xml | 1 + .../Model/SynchronizerIdentitiesPool.php | 47 ++++++++ .../SynchronizeIdentitiesCatalog.php | 15 ++- .../etc/di.xml | 9 ++ .../MediaContentSynchronizationCms/etc/di.xml | 9 ++ 6 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php new file mode 100644 index 0000000000000..0cd9b9bca6784 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool; +use Psr\Log\LoggerInterface; + +/** + * Batch Synchronize content with assets + */ +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; + + /** + * @var DateTimeFactory + */ + private $dateFactory; + + /** + * @var FlagManager + */ + private $flagManager; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizerIdentitiesPool + */ + private $synchronizerIdentitiesPool; + + /** + * @var RemoveObsoleteContentAsset + */ + private $removeObsoleteContent; + + /** + * @param RemoveObsoleteContentAsset $removeObsoleteContent + * @param DateTimeFactory $dateFactory + * @param FlagManager $flagManager + * @param LoggerInterface $log + * @param SynchronizerIdentitiesPool $synchronizerIdentitiesPool + */ + public function __construct( + RemoveObsoleteContentAsset $removeObsoleteContent, + DateTimeFactory $dateFactory, + FlagManager $flagManager, + LoggerInterface $log, + SynchronizerIdentitiesPool $synchronizerIdentitiesPool + ) { + $this->removeObsoleteContent = $removeObsoleteContent; + $this->dateFactory = $dateFactory; + $this->flagManager = $flagManager; + $this->log = $log; + $this->synchronizerIdentitiesPool = $synchronizerIdentitiesPool; + } + + /** + * @inheritdoc + */ + public function execute(array $mediaContentIdentities): void + { + $failed = []; + + foreach ($this->synchronizerIdentitiesPool->get() as $name => $synchronizers) { + try { + $synchronizers->execute($mediaContentIdentities); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following content synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + + $this->setLastExecutionTime(); + $this->removeObsoleteContent->execute(); + } + + /** + * Set last synchronizer execution time + */ + private function setLastExecutionTime(): void + { + $currentTime = $this->dateFactory->create()->gmtDate(); + $this->flagManager->saveFlag(self::LAST_EXECUTION_TIME_CODE, $currentTime); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index d4615c15206e5..e5347f1a11561 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaContentSynchronization\Model\Synchronize"/> + <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface" type="Magento\MediaContentSynchronization\Model\SynchronizeIdentities"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php new file mode 100644 index 0000000000000..2f4a503625e91 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizerIdentitiesPool +{ + /** + * Content with assets synchronizers + * + * @var SynchronizeIdentitiesInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeIdentitiesInterface[] $synchronizers + */ + public function __construct( + array $synchronizers = [] + ) { + foreach ($synchronizers as $synchronizer) { + if (!$synchronizer instanceof SynchronizeIdentitiesInterface) { + throw new \InvalidArgumentException( + get_class($synchronizer) . ' must implement ' . SynchronizeIdentitiesInterface::class + ); + } + } + + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeIdentitiesInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php index 068d733e14605..a57b8eb2041cb 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php @@ -13,6 +13,9 @@ class SynchronizeIdentitiesCatalog implements SynchronizeIdentitiesInterface { + private const FIELD_CATALOG_PRODUCT = 'catalog_product'; + private const FIELD_CATALOG_CATEGORY = 'catalog_category'; + /** * @var UpdateContentAssetLinksInterface */ @@ -41,10 +44,14 @@ public function __construct( public function execute(array $mediaContentIdentities): void { foreach ($mediaContentIdentities as $identity) { - $this->updateContentAssetLinks->execute( - $identity, - implode(PHP_EOL, $this->getEntityContents->execute($identity)) - ); + if ($identity->getEntityType() === self::FIELD_CATALOG_PRODUCT + || $identity->getEntityType() === self::FIELD_CATALOG_CATEGORY + ) { + $this->updateContentAssetLinks->execute( + $identity, + implode(PHP_EOL, $this->getEntityContents->execute($identity)) + ); + } } } } diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml index 8cc86fde8fbcd..7c391faf3f955 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -38,4 +38,13 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_catalog_synchronizeIdentity" + xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\SynchronizeIdentitiesCatalog + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml index 7def330298789..43a863dc95764 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -14,6 +14,15 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_cms_synchronizeIdentity" + xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\SynchronizeIdentitiesCms + </item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface"> <arguments> <argument name="entities" xsi:type="array"> From dd9bdda933e36b519a7f0a8d52579bfb48bf2a33 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 28 Aug 2020 16:09:12 +0300 Subject: [PATCH 34/96] Improve acl resources for new media gallery --- .../Adminhtml/Wysiwyg/Images/DeleteFiles.php | 5 -- .../Adminhtml/Wysiwyg/Images/DeleteFolder.php | 6 --- .../Adminhtml/Wysiwyg/Images/NewFolder.php | 5 -- .../Adminhtml/Wysiwyg/Images/OnInsert.php | 4 -- .../Adminhtml/Wysiwyg/Images/Upload.php | 5 -- app/code/Magento/MediaGallery/etc/acl.xml | 26 ++++++++++ .../Adminhtml/Directories/Create.php | 2 +- .../Adminhtml/Directories/Delete.php | 2 +- .../Controller/Adminhtml/Image/Delete.php | 2 +- .../Controller/Adminhtml/Image/Upload.php | 2 +- .../Ui/Component/Control/CreateFolder.php | 50 ++++++++++++++++++ .../Ui/Component/Control/DeleteAssets.php | 50 ++++++++++++++++++ .../Ui/Component/Control/DeleteFolder.php | 51 +++++++++++++++++++ .../Ui/Component/Control/UploadAssets.php | 51 +++++++++++++++++++ app/code/Magento/MediaGalleryUi/etc/acl.xml | 26 ---------- .../ui_component/media_gallery_listing.xml | 29 ++--------- .../standalone_media_gallery_listing.xml | 28 ++-------- 17 files changed, 240 insertions(+), 104 deletions(-) create mode 100644 app/code/Magento/MediaGallery/etc/acl.xml create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php delete mode 100644 app/code/Magento/MediaGalleryUi/etc/acl.xml diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php index 848a10950e0ba..fa873930aaade 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php @@ -13,11 +13,6 @@ */ class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_Cms::delete_assets'; - /** * @var \Magento\Framework\Controller\Result\JsonFactory */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index 8a67d9220e11c..1f991bb47c6fd 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -10,18 +10,12 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\App\Filesystem\DirectoryList; /** * Delete image folder. */ class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_Cms::delete_folder'; - /** * @var \Magento\Framework\Controller\Result\JsonFactory */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index de462f82c8911..706718455a523 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -14,11 +14,6 @@ */ class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_Cms::create_folder'; - /** * @var \Magento\Framework\Controller\Result\JsonFactory */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php index 516742ffd2ee8..c86f6970d2495 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php @@ -8,10 +8,6 @@ class OnInsert extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_Cms::insert_assets'; /** * @var \Magento\Framework\Controller\Result\RawFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 6e48dfe7c5f10..260755ea7d562 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -17,11 +17,6 @@ */ class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_Cms::upload_assets'; - /** * @var \Magento\Framework\Controller\Result\JsonFactory */ diff --git a/app/code/Magento/MediaGallery/etc/acl.xml b/app/code/Magento/MediaGallery/etc/acl.xml new file mode 100644 index 0000000000000..a90d43e2a6cd2 --- /dev/null +++ b/app/code/Magento/MediaGallery/etc/acl.xml @@ -0,0 +1,26 @@ +<?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:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Backend::content"> + <resource id="Magento_Backend::content_elements"> + <resource id="Magento_Cms::media_gallery" title="Media Gallery" translate="title"> + <resource id="Magento_MediaGallery::upload_assets" title="Upload Assets" translate="title" sortOrder="80"/> + <resource id="Magento_MediaGallery::delete_assets" title="Delete Assets" translate="title" sortOrder="70"/> + <resource id="Magento_MediaGallery::insert_assets" title="Insert Assets into the content" translate="title" sortOrder="60"/> + <resource id="Magento_MediaGallery::create_folder" title="Create Folder" translate="title" sortOrder="50"/> + <resource id="Magento_MediaGallery::delete_folder" title="Delete Folder" translate="title" sortOrder="40"/> + </resource> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php index 147fc48910113..f303cd4b7d5e5 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::create_folder'; + public const ADMIN_RESOURCE = 'Magento_MediaGallery::create_folder'; /** * @var CreateDirectoriesByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php index 1ca2512826504..f11fa36c0e499 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::delete_folder'; + public const ADMIN_RESOURCE = 'Magento_MediaGallery::delete_folder'; /** * @var DeleteAssetsByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php index d630d93688d28..30fc9a772cafd 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::delete_assets'; + public const ADMIN_RESOURCE = 'Magento_MediaGallery::delete_assets'; /** * @var DeleteImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php index 0d3f9a78ae3ca..11df39bd2b1dc 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::upload_assets'; + public const ADMIN_RESOURCE = 'Magento_MediaGallery::upload_assets'; /** * @var UploadImage diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php new file mode 100644 index 0000000000000..e4cc00e0bb107 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Create Folder button + */ +class CreateFolder implements ButtonProviderInterface +{ + private const ACL_CREATE_FOLDER = 'Magento_MediaGallery::create_folder'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData() + { + if (!$this->authorization->isAllowed(self::ACL_CREATE_FOLDER)) { + return []; + } + + return [ + 'label' => __('Create Folder'), + 'on_click' => 'jQuery("#create_folder").trigger("create_folder");', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 10, + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php new file mode 100644 index 0000000000000..e31f35e9e28fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Delete images button + */ +class DeleteAssets implements ButtonProviderInterface +{ + private const ACL_DELETE_ASSETS= 'Magento_MediaGallery::delete_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @return array + */ + public function getButtonData() + { + if (!$this->authorization->isAllowed(self::ACL_DELETE_ASSETS)) { + return []; + } + + return [ + 'label' => __('Delete Images...'), + 'on_click' => 'jQuery(window).trigger("massAction.MediaGallery")', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 50, + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php new file mode 100644 index 0000000000000..97c336de21cd6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Delete Folder button + */ +class DeleteFolder implements ButtonProviderInterface +{ + private const ACL_DELETE_FOLDER = 'Magento_MediaGallery::delete_folder'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @return array + */ + public function getButtonData() + { + if (!$this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { + return []; + } + + return [ + 'label' => __('Delete Folder'), + 'disabled' => 'disabled', + 'on_click' => 'jQuery("#delete_folder").trigger("delete_folder");', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 30, + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php new file mode 100644 index 0000000000000..5be3263f63316 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Upload Image button + */ +class UploadAssets implements ButtonProviderInterface +{ + private const ACL_UPLOAD_ASSETS= 'Magento_MediaGallery::upload_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @return array + */ + public function getButtonData() + { + if (!$this->authorization->isAllowed(self::ACL_UPLOAD_ASSETS)) { + return []; + } + + return [ + 'label' => __('Upload Image'), + 'disabled' => 'disabled', + 'on_click' => 'jQuery("#image-uploader-input").click();', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 20, + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/acl.xml b/app/code/Magento/MediaGalleryUi/etc/acl.xml deleted file mode 100644 index 7c8784e412814..0000000000000 --- a/app/code/Magento/MediaGalleryUi/etc/acl.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?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:Acl/etc/acl.xsd"> - <acl> - <resources> - <resource id="Magento_Backend::admin"> - <resource id="Magento_Backend::content"> - <resource id="Magento_Backend::content_elements"> - <resource id="Magento_Cms::media_gallery"> - <resource id="Magento_Cms::upload_assets" title="Upload Assets" translate="title" sortOrder="70"/> - <resource id="Magento_Cms::delete_assets" title="Delete Assets" translate="title" sortOrder="60"/> - <resource id="Magento_Cms::insert_assets" title="Insert Assets into the content" translate="title" sortOrder="50"/> - <resource id="Magento_Cms::create_folder" title="Create Folder" translate="title" sortOrder="40"/> - <resource id="Magento_Cms::delete_folder" title="Delete Folder" translate="title" sortOrder="40"/> - </resource> - </resource> - </resource> - </resource> - </resources> - </acl> -</config> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index 49206043725f9..a886bcc1c3374 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -28,31 +28,10 @@ <class>cancel action-quaternary</class> <label translate="true">Cancel</label> </button> - <button name="upload_image"> - <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> - <class>action-add scalable media-gallery-actions-buttons</class> - <param name="sort_order" xsi:type="number">20</param> - <label translate="true">Upload Image</label> - </button> - <button name="delete_folder"> - <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> - <param name="disabled" xsi:type="string">disabled</param> - <param name="sort_order" xsi:type="number">30</param> - <class>action-default scalable media-gallery-actions-buttons</class> - <label translate="true">Delete Folder</label> - </button> - <button name="create_folder"> - <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> - <param name="sort_order" xsi:type="number">10</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Create Folder</label> - </button> - <button name="delete_massaction"> - <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> - <param name="sort_order" xsi:type="number">50</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Images...</label> - </button> + <button name="upload_image" class="Magento\MediaGalleryUi\Ui\Component\Control\UploadAssets"/> + <button name="delete_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteFolder"/> + <button name="create_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\CreateFolder"/> + <button name="delete_massaction" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteAssets"/> </buttons> <spinner>media_gallery_columns</spinner> <deps> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index 655178c104492..bbb62a53f0af3 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -20,30 +20,10 @@ <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> </deps> <buttons> - <button name="delete_folder"> - <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> - <param name="disabled" xsi:type="string">disabled</param> - <param name="sort_order" xsi:type="number">20</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Folder</label> - </button> - <button name="create_folder"> - <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> - <param name="sort_order" xsi:type="number">30</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Create Folder</label> - </button> - <button name="delete_massaction"> - <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> - <param name="sort_order" xsi:type="number">50</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Images...</label> - </button> - <button name="upload_image"> - <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Upload Image</label> - </button> + <button name="upload_image" class="Magento\MediaGalleryUi\Ui\Component\Control\UploadAssets"/> + <button name="delete_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteFolder"/> + <button name="create_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\CreateFolder"/> + <button name="delete_massaction" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteAssets"/> </buttons> </settings> <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> From ae3d3df4752dfcac5256ddb5014ce02b19e8a529 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 28 Aug 2020 16:28:54 +0300 Subject: [PATCH 35/96] fix widget with special char page tittle --- .../Widget/Block/Adminhtml/Widget/Options.php | 5 - app/code/Magento/Widget/Model/Widget.php | 197 ++++++++---------- 2 files changed, 85 insertions(+), 117 deletions(-) diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php index 44c43055df8b9..32bae10c801c8 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php @@ -136,7 +136,6 @@ public function addFields() * @return \Magento\Framework\Data\Form\Element\AbstractElement * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _addField($parameter) { @@ -159,10 +158,6 @@ protected function _addField($parameter) $data['value'] = $parameter->getValue(); } - if ($parameter->getType() == 'text' && $data['value'] != '') { - $data['value'] = $this->_widget->decodeReservedChars($data['value']); - } - //prepare unique id value if ($fieldName == 'unique_id' && $data['value'] == '') { $data['value'] = hash('sha256', microtime(1)); diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 3ca52cec43f40..6d9bb1878708a 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -5,6 +5,16 @@ */ namespace Magento\Widget\Model; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Asset\Repository; +use Magento\Framework\View\Asset\Source; +use Magento\Framework\View\FileSystem; +use Magento\Widget\Helper\Conditions; +use Magento\Widget\Model\Config\Data; + /** * Widget model for different purposes * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -15,32 +25,32 @@ class Widget { /** - * @var \Magento\Widget\Model\Config\Data + * @var Data */ protected $dataStorage; /** - * @var \Magento\Framework\App\Cache\Type\Config + * @var Config */ protected $configCacheType; /** - * @var \Magento\Framework\View\Asset\Repository + * @var Repository */ protected $assetRepo; /** - * @var \Magento\Framework\View\Asset\Source + * @var Source */ protected $assetSource; /** - * @var \Magento\Framework\View\FileSystem + * @var FileSystem */ protected $viewFileSystem; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $escaper; @@ -50,30 +60,35 @@ class Widget protected $widgetsArray = []; /** - * @var \Magento\Widget\Helper\Conditions + * @var Conditions */ protected $conditionsHelper; /** - * @var \Magento\Framework\Math\Random + * @var Random */ private $mathRandom; /** - * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Widget\Model\Config\Data $dataStorage - * @param \Magento\Framework\View\Asset\Repository $assetRepo - * @param \Magento\Framework\View\Asset\Source $assetSource - * @param \Magento\Framework\View\FileSystem $viewFileSystem - * @param \Magento\Widget\Helper\Conditions $conditionsHelper + * @var string[] + */ + private $reservedChars = ['}', '{']; + + /** + * @param Escaper $escaper + * @param Data $dataStorage + * @param Repository $assetRepo + * @param Source $assetSource + * @param FileSystem $viewFileSystem + * @param Conditions $conditionsHelper */ public function __construct( - \Magento\Framework\Escaper $escaper, - \Magento\Widget\Model\Config\Data $dataStorage, - \Magento\Framework\View\Asset\Repository $assetRepo, - \Magento\Framework\View\Asset\Source $assetSource, - \Magento\Framework\View\FileSystem $viewFileSystem, - \Magento\Widget\Helper\Conditions $conditionsHelper + Escaper $escaper, + Data $dataStorage, + Repository $assetRepo, + Source $assetSource, + FileSystem $viewFileSystem, + Conditions $conditionsHelper ) { $this->escaper = $escaper; $this->dataStorage = $dataStorage; @@ -110,14 +125,11 @@ public function getWidgetByClassType($type) $widgets = $this->getWidgets(); /** @var array $widget */ foreach ($widgets as $widget) { - if (isset($widget['@'])) { - if (isset($widget['@']['type'])) { - if ($type === $widget['@']['type']) { - return $widget; - } - } + if (isset($widget['@']['type']) && $type === $widget['@']['type']) { + return $widget; } } + return null; } @@ -131,8 +143,7 @@ public function getWidgetByClassType($type) */ public function getConfigAsXml($type) { - // phpstan:ignore - return $this->getXmlElementByType($type); + return $this->getWidgetByClassType($type); } /** @@ -294,48 +305,71 @@ public function getWidgetsArray($filters = []) * @param array $params Pre-configured Widget Params * @param bool $asIs Return result as widget directive(true) or as placeholder image(false) * @return string Widget directive ready to parse - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getWidgetDeclaration($type, $params = [], $asIs = true) { - $directive = '{{widget type="' . $type . '"'; $widget = $this->getConfigAsObject($type); + $directiveParams = ''; foreach ($params as $name => $value) { // Retrieve default option value if pre-configured - if ($name == 'conditions') { - $name = 'conditions_encoded'; - $value = $this->conditionsHelper->encode($value); - } elseif ($this->isTextType($widget, $name)) { - $value = $this->encodeReservedChars($value); - } elseif (is_array($value)) { - $value = implode(',', $value); - } elseif (trim($value) == '') { - $parameters = $widget->getParameters(); - if (isset($parameters[$name]) && is_object($parameters[$name])) { - $value = $parameters[$name]->getValue(); - } - } - if (isset($value)) { - $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); - } + $directiveParams .= $this->getDirectiveParam($widget, $name, $value); } - $directive .= $this->getWidgetPageVarName($params); - - $directive .= '}}'; + $directive = sprintf('{{widget type="%s"%s%s}}', $type, $directiveParams, $this->getWidgetPageVarName($params)); if ($asIs) { return $directive; } - $html = sprintf( + return sprintf( '<img id="%s" src="%s" title="%s">', $this->idEncode($directive), $this->getPlaceholderImageUrl($type), $this->escaper->escapeUrl($directive) ); - return $html; + } + + /** + * Returns directive param with prepared value + * + * @param DataObject $widget + * @param string $name + * @param string|array $value + * @return string + */ + private function getDirectiveParam(DataObject $widget, string $name, $value): string + { + if ($name === 'conditions') { + $name = 'conditions_encoded'; + $value = $this->conditionsHelper->encode($value); + } elseif (is_array($value)) { + $value = implode(',', $value); + } elseif (trim($value) === '') { + $parameters = $widget->getParameters(); + if (isset($parameters[$name]) && is_object($parameters[$name])) { + $value = $parameters[$name]->getValue(); + } + } else { + $value = $this->getPreparedValue($value); + } + + return $value !== null + ? sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)) + : ''; + } + + /** + * Returns encoded value if it contains reserved chars + * + * @param string $value + * @return string + */ + private function getPreparedValue(string $value): string + { + $pattern = sprintf('/%s/', implode('|', $this->reservedChars)); + + return preg_match($pattern, $value) ? rawurlencode($value) : $value; } /** @@ -460,65 +494,4 @@ protected function sortParameters($firstElement, $secondElement) $bOrder = (int)$secondElement->getData('sort_order'); return $aOrder < $bOrder ? -1 : ($aOrder > $bOrder ? 1 : 0); } - - /** - * Encode reserved chars - * - * @param string $string - * @return string|string[] - */ - private function encodeReservedChars($string) - { - $map = [ - '{' => urlencode('{'), - '}' => urlencode('}') - ]; - - return str_replace( - array_keys($map), - array_values($map), - $string - ); - } - - /** - * Decode reserved chars - * - * @param string $string - * @return array - */ - public function decodeReservedChars($string) - { - $map = [ - '{' => urlencode('{'), - '}' => urlencode('}') - ]; - - return str_replace( - array_values($map), - array_keys($map), - $string - ); - } - - /** - * Is text type Widget parameter - * - * @param \Magento\Framework\DataObject $widget - * @param string $name - * @return bool - */ - private function isTextType($widget, $name) - { - $parameters = $widget->getParameters(); - - if (isset($parameters[$name]) && is_object($parameters[$name])) { - $type = $parameters[$name]->getType(); - if ($type == 'text') { - return true; - } - } - - return false; - } } From 989139f0fc77bbb1c1eac28985955145b83ae3fb Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 28 Aug 2020 17:04:00 +0300 Subject: [PATCH 36/96] Improve button visibility --- .../Ui/Component/Control/CreateFolder.php | 12 +++-- .../Ui/Component/Control/DeleteAssets.php | 14 ++--- .../Ui/Component/Control/DeleteFolder.php | 11 ++-- .../Ui/Component/Control/InsertAsstes.php | 52 +++++++++++++++++++ .../Ui/Component/Control/UploadAssets.php | 12 +++-- .../Ui/Component/DirectoriesTree.php | 1 + .../ui_component/media_gallery_listing.xml | 7 +-- .../adminhtml/web/js/directory/directories.js | 4 ++ .../web/js/grid/massaction/massactions.js | 5 ++ 9 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php index e4cc00e0bb107..a0f7ebb623991 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -36,15 +36,17 @@ public function __construct( */ public function getButtonData() { - if (!$this->authorization->isAllowed(self::ACL_CREATE_FOLDER)) { - return []; - } - - return [ + $buttonData = [ 'label' => __('Create Folder'), 'on_click' => 'jQuery("#create_folder").trigger("create_folder");', 'class' => 'action-default scalable add media-gallery-actions-buttons', 'sort_order' => 10, ]; + + if (!$this->authorization->isAllowed(self::ACL_CREATE_FOLDER)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php index e31f35e9e28fb..8e3b033d4d927 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -34,17 +34,19 @@ public function __construct( /** * @return array */ - public function getButtonData() + public function getButtonData(): array { - if (!$this->authorization->isAllowed(self::ACL_DELETE_ASSETS)) { - return []; - } - - return [ + $buttonData = [ 'label' => __('Delete Images...'), 'on_click' => 'jQuery(window).trigger("massAction.MediaGallery")', 'class' => 'action-default scalable add media-gallery-actions-buttons', 'sort_order' => 50, ]; + + if (!$this->authorization->isAllowed(self::ACL_DELETE_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php index 97c336de21cd6..65986f9ed4b98 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -36,16 +36,17 @@ public function __construct( */ public function getButtonData() { - if (!$this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { - return []; - } - - return [ + $buttonData = [ 'label' => __('Delete Folder'), 'disabled' => 'disabled', 'on_click' => 'jQuery("#delete_folder").trigger("delete_folder");', 'class' => 'action-default scalable add media-gallery-actions-buttons', 'sort_order' => 30, ]; + if (!$this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php new file mode 100644 index 0000000000000..25846851ed2a6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Add selected button + */ +class InsertAsstes implements ButtonProviderInterface +{ + private const ACL_INSERT_ASSETS = 'Magento_MediaGallery::insert_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData() + { + $buttonData = [ + 'label' => __('Add Selected'), + 'on_click' => 'return false;");', + 'class' => 'action-primary no-display media-gallery-add-selected', + 'sort_order' => 110, + ]; + + if (!$this->authorization->isAllowed(self::ACL_INSERT_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php index 5be3263f63316..08cfe2386ad92 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -36,16 +36,18 @@ public function __construct( */ public function getButtonData() { - if (!$this->authorization->isAllowed(self::ACL_UPLOAD_ASSETS)) { - return []; - } - - return [ + $buttonData = [ 'label' => __('Upload Image'), 'disabled' => 'disabled', 'on_click' => 'jQuery("#image-uploader-input").click();', 'class' => 'action-default scalable add media-gallery-actions-buttons', 'sort_order' => 20, ]; + + if (!$this->authorization->isAllowed(self::ACL_UPLOAD_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php index 4047a4fcb98d8..5798a788a03f4 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php @@ -50,6 +50,7 @@ public function prepare(): void array_replace_recursive( (array) $this->getData('config'), [ + 'allowedActions' => [], 'getDirectoryTreeUrl' => $this->url->getUrl("media_gallery/directories/gettree"), 'deleteDirectoryUrl' => $this->url->getUrl("media_gallery/directories/delete"), 'createDirectoryUrl' => $this->url->getUrl("media_gallery/directories/create") diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index a886bcc1c3374..58e5435faaf4d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -16,12 +16,7 @@ </argument> <settings> <buttons> - <button name="add_selected"> - <param name="on_click" xsi:type="string">return false;</param> - <param name="sort_order" xsi:type="number">110</param> - <class>action-primary no-display media-gallery-add-selected</class> - <label translate="true">Add Selected</label> - </button> + <button name="add_selected" class="Magento\MediaGalleryUi\Ui\Component\Control\InsertAsstes"/> <button name="cancel"> <param name="on_click" xsi:type="string">MediabrowserUtility.closeDialog();</param> <param name="sort_order" xsi:type="number">1</param> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js index d7f756d8bbd90..5e82f05a3c261 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -179,6 +179,10 @@ define([ * @param {String} folderId */ setActive: function (folderId) { + if (!$.inArray('delete_folder', this.allowedActions)) { + return; + } + this.selectedFolder(folderId); $(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled'); } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js index 4f09854005f23..cd8a598b9ef67 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -16,6 +16,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], deleteButtonSelector: '#delete_selected_massaction', deleteImagesSelector: '#delete_massaction', mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', @@ -106,6 +107,10 @@ define([ * If images records less than one, disable "delete images" button */ checkButtonVisibility: function () { + if (!$.inArray('delete_assets', this.allowedActions)) { + return; + } + if (this.imageItems.length < 1) { $(this.deleteImagesSelector).addClass('disabled'); } else { From 239314d98653081f02752ee5742136b1f3ba07e2 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 28 Aug 2020 19:16:17 +0300 Subject: [PATCH 37/96] Improve button visibility per allowed actions for user --- .../Ui/Component/Control/CreateFolder.php | 2 +- .../Ui/Component/Control/DeleteAssets.php | 2 +- .../Ui/Component/Control/DeleteFolder.php | 4 +-- .../Ui/Component/Control/InsertAsstes.php | 4 +-- .../Ui/Component/Control/UploadAssets.php | 4 +-- .../Ui/Component/Listing/Columns/Url.php | 30 +++++++++++++++++++ .../adminhtml/templates/image_details.phtml | 2 -- .../templates/image_details_standalone.phtml | 2 -- .../adminhtml/web/css/source/_module.less | 3 ++ .../adminhtml/web/js/directory/directories.js | 3 +- .../adminhtml/web/js/grid/columns/image.js | 13 ++++++-- .../web/js/grid/columns/image/actions.js | 15 +++++++++- .../web/js/grid/massaction/massactions.js | 2 +- .../template/grid/columns/image/actions.html | 6 ++-- 14 files changed, 72 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php index a0f7ebb623991..b05bf116e61c6 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -34,7 +34,7 @@ public function __construct( /** * @inheritdoc */ - public function getButtonData() + public function getButtonData(): array { $buttonData = [ 'label' => __('Create Folder'), diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php index 8e3b033d4d927..fd5564c44ff89 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -32,7 +32,7 @@ public function __construct( } /** - * @return array + * @inheritdoc */ public function getButtonData(): array { diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php index 65986f9ed4b98..2fa14f19b197d 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -32,9 +32,9 @@ public function __construct( } /** - * @return array + * @inheritdoc */ - public function getButtonData() + public function getButtonData(): array { $buttonData = [ 'label' => __('Delete Folder'), diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php index 25846851ed2a6..4bd901da823fd 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php @@ -34,11 +34,11 @@ public function __construct( /** * @inheritdoc */ - public function getButtonData() + public function getButtonData(): array { $buttonData = [ 'label' => __('Add Selected'), - 'on_click' => 'return false;");', + 'on_click' => 'return false;', 'class' => 'action-primary no-display media-gallery-add-selected', 'sort_order' => 110, ]; diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php index 08cfe2386ad92..3e7756d319c8a 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -32,9 +32,9 @@ public function __construct( } /** - * @return array + * @inheritdoc */ - public function getButtonData() + public function getButtonData(): array { $buttonData = [ 'label' => __('Upload Image'), diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php index 481f8ab861f0f..7a6f59a75f1cf 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -15,12 +15,18 @@ use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\AuthorizationInterface; /** * Overlay column */ class Url extends Column { + private const ACL_IMAGE_ACTIONS = [ + 'insert_assets' => 'Magento_MediaGallery::insert_assets', + 'delete_assets' => 'Magento_MediaGallery::delete_assets' + ]; + /** * @var StoreManagerInterface */ @@ -41,6 +47,11 @@ class Url extends Column */ private $storage; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory @@ -48,6 +59,7 @@ class Url extends Column * @param UrlInterface $urlInterface * @param Images $images * @param Storage $storage + * @param AuthorizationInterface $authorization * @param array $components * @param array $data */ @@ -58,6 +70,7 @@ public function __construct( UrlInterface $urlInterface, Images $images, Storage $storage, + AuthorizationInterface $authorization, array $components = [], array $data = [] ) { @@ -66,6 +79,7 @@ public function __construct( $this->urlInterface = $urlInterface; $this->images = $images; $this->storage = $storage; + $this->authorization = $authorization; } /** @@ -98,6 +112,7 @@ public function prepare(): void array_replace_recursive( (array)$this->getData('config'), [ + 'allowedActions' => $this->getAllowedActions(), 'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'), 'storeId' => $this->storeManager->getStore()->getId() ] @@ -105,6 +120,21 @@ public function prepare(): void ); } + /** + * Return allowed actions for media gallery image + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } + /** * Get URL for the provided media asset path * diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml index ba2033478afa1..67733b2c6855d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -105,5 +105,3 @@ use Magento\Framework\Escaper; } } </script> - - diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml index 9fc0e749ac888..f0b653cb8cd14 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -97,5 +97,3 @@ use Magento\Backend\Block\Template; } } </script> - - diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less index fc8bd49126d8e..71677380c1bcc 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -99,6 +99,9 @@ .media-gallery-container { + .action-disabled { + opacity: 0.5; + } .masonry-image-grid .no-data-message-container, .masonry-image-grid .error-message-container { left: 50%; diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js index 5e82f05a3c261..d81c05d805378 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -19,6 +19,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], directoryTreeSelector: '#media-gallery-directory-tree', deleteButtonSelector: '#delete_folder', createFolderButtonSelector: '#create_folder', @@ -179,7 +180,7 @@ define([ * @param {String} folderId */ setActive: function (folderId) { - if (!$.inArray('delete_folder', this.allowedActions)) { + if (!this.allowedActions.includes('delete_folder')) { return; } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js index bf852d0ddae68..ca731e693bd2d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -17,6 +17,7 @@ define([ addSelectedBtnSelector: '#add_selected', deleteSelectedBtnSelector: '#delete_selected', selected: null, + allowedActions: [], fields: { id: 'id', url: 'url', @@ -39,7 +40,8 @@ define([ { component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', name: '${ $.name }_actions', - imageModelName: '${ $.name }' + imageModelName: '${ $.name }', + allowedActions: '${ $.allowedActions }' } ] }, @@ -222,8 +224,15 @@ define([ toggleAddSelectedButton: function () { if (this.selected() === null) { this.hideAddSelectedAndDeleteButon(); - } else { + + return; + } + + if (this.allowedActions.includes('insert_assets')) { $(this.addSelectedBtnSelector).removeClass('no-display'); + } + + if (this.allowedActions.includes('delete_assets')) { $(this.deleteSelectedBtnSelector).removeClass('no-display'); } }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js index 38743c8d83d3b..74e9b6ac8ecb7 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -8,7 +8,8 @@ define([ 'uiComponent', 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', - 'mage/translate' + 'mage/translate', + 'Magento_Ui/js/lib/view/utils/async' ], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { 'use strict'; @@ -17,20 +18,24 @@ define([ template: 'Magento_MediaGalleryUi/grid/columns/image/actions', mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + allowedActions: [], actionsList: [ { name: 'image-details', title: $t('View Details'), + classes: 'action-menu-item', handler: 'viewImageDetails' }, { name: 'edit', title: $t('Edit'), + classes: 'action-menu-item', handler: 'editImageDetails' }, { name: 'delete', title: $t('Delete'), + classes: 'action-menu-item media-gallery-delete-assets', handler: 'deleteImageAction' } ], @@ -50,6 +55,14 @@ define([ this._super(); this.initEvents(); + if (!this.allowedActions.includes('delete_assets')) { + $.async('.media-gallery-delete-assets', function () { + $('.media-gallery-delete-assets').attr('click', '#') + .unbind('click') + .addClass('action-disabled'); + }); + } + return this; }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js index cd8a598b9ef67..b16db2873cfc8 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -107,7 +107,7 @@ define([ * If images records less than one, disable "delete images" button */ checkButtonVisibility: function () { - if (!$.inArray('delete_assets', this.allowedActions)) { + if (!this.allowedActions.includes('delete_assets')) { return; } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html index 042e119b9f40e..72447196cea55 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -7,9 +7,9 @@ <each args="{ data: actionsList, as: 'action' }"> <li> - <a class="action-menu-item" href="" text="action.title" + <a href="#" text="action.title" click="$parent[action.handler].bind($parent, $row())" - attr="{'data-action': 'item-' + action.name}"> + attr="{'data-action': 'item-' + action.name, class: action.classes}"> </a> </li> -</each> \ No newline at end of file +</each> From 8cdcefd99c6d8d5ac20d6d430302eac97aaed6f2 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 28 Aug 2020 21:53:51 +0300 Subject: [PATCH 38/96] Improve buttons visibility per user permission --- .../Ui/Component/DirectoryTree.php | 27 ++++++- .../Listing/Massactions/Massaction.php | 77 +++++++++++++++++++ .../ui_component/media_gallery_listing.xml | 1 + .../standalone_media_gallery_listing.xml | 1 + .../web/js/directory/directoryTree.js | 4 +- 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index c1429595faf56..c7c12d0225504 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -10,33 +10,44 @@ use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Container; +use Magento\Framework\AuthorizationInterface; /** * Directories tree component */ class DirectoryTree extends Container { + private const ACL_DELETE_FOLDER = 'Magento_MediaGallery::delete_folder'; + /** * @var UrlInterface */ private $url; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * Constructor * * @param ContextInterface $context * @param UrlInterface $url + * @param AuthorizationInterface $authorization * @param array $components * @param array $data */ public function __construct( ContextInterface $context, UrlInterface $url, + AuthorizationInterface $authorization, array $components = [], array $data = [] ) { parent::__construct($context, $components, $data); $this->url = $url; + $this->authorization = $authorization; } /** @@ -50,7 +61,7 @@ public function prepare(): void array_replace_recursive( (array) $this->getData('config'), [ - 'allowedActions' => [], + 'allowedActions' => $this->getAllowedActions(), 'getDirectoryTreeUrl' => $this->url->getUrl('media_gallery/directories/gettree'), 'deleteDirectoryUrl' => $this->url->getUrl('media_gallery/directories/delete'), 'createDirectoryUrl' => $this->url->getUrl('media_gallery/directories/create') @@ -58,4 +69,18 @@ public function prepare(): void ) ); } + + /** + * Return allowed actions for media gallery + */ + private function getAllowedActions(): array + { + $allowedActions = []; + + if ($this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { + $allowedActions[] = 'delete_folder'; + } + + return $allowedActions; + } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php new file mode 100644 index 0000000000000..c16d90116bca0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Massactions; + +use Magento\Ui\Component\Container; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Massaction comntainer + */ +class Massaction extends Container +{ + private const ACL_IMAGE_ACTIONS = [ + 'delete_assets' => 'Magento_MediaGallery::delete_assets' + ]; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param ContextInterface $context + * @param AuthorizationInterface $authorization + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + AuthorizationInterface $authorization, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'allowedActions' => $this->getAllowedActions() + ] + ) + ); + } + + /** + * Return allowed actions for media gallery + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } +} diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index 7ea952280207d..b7307f9a74fae 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -181,6 +181,7 @@ <container name="media_gallery_massactions" displayArea="sorting" sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Massactions\Massaction" component="Magento_MediaGalleryUi/js/grid/massaction/massactions" template="Magento_MediaGalleryUi/grid/massactions/count" > <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index 66fbd91507ae7..a53a46c61f75d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -174,6 +174,7 @@ <container name="media_gallery_massactions" displayArea="sorting" sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Massactions\Massaction" component="Magento_MediaGalleryUi/js/grid/massaction/massactions" template="Magento_MediaGalleryUi/grid/massactions/count" > <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js index 2e1e9a980cd59..84f253da826a8 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -17,6 +17,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', directoryTreeSelector: '#media-gallery-directory-tree', getDirectoryTreeUrl: 'media_gallery/directories/gettree', @@ -32,7 +33,8 @@ define([ }, viewConfig: [{ component: 'Magento_MediaGalleryUi/js/directory/directories', - name: '${ $.name }_directories' + name: '${ $.name }_directories', + allowedActions: '${ $.allowedActions }' }] }, From 43fd0573739a014277f99141f0cc58376fc8217a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 28 Aug 2020 22:01:15 +0300 Subject: [PATCH 39/96] Code style improvements --- .../MediaGalleryUi/Ui/Component/DirectoryTree.php | 12 ++++++++---- .../adminhtml/web/js/grid/columns/image/actions.js | 4 +--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index c7c12d0225504..2fc662a66e14c 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -17,7 +17,9 @@ */ class DirectoryTree extends Container { - private const ACL_DELETE_FOLDER = 'Magento_MediaGallery::delete_folder'; + private const ACL_IMAGE_ACTIONS = [ + 'delete_folder' => 'Magento_MediaGallery::delete_folder' + ]; /** * @var UrlInterface @@ -70,15 +72,17 @@ public function prepare(): void ); } + /** * Return allowed actions for media gallery */ private function getAllowedActions(): array { $allowedActions = []; - - if ($this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { - $allowedActions[] = 'delete_folder'; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } } return $allowedActions; diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js index 74e9b6ac8ecb7..ec959a06e1cce 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -57,9 +57,7 @@ define([ if (!this.allowedActions.includes('delete_assets')) { $.async('.media-gallery-delete-assets', function () { - $('.media-gallery-delete-assets').attr('click', '#') - .unbind('click') - .addClass('action-disabled'); + $('.media-gallery-delete-assets').unbind('click').addClass('action-disabled'); }); } From dfa67b0d6c6dd4050ad4b21b2f7c23c81ef8b7e5 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Sat, 29 Aug 2020 00:05:51 +0300 Subject: [PATCH 40/96] Static test fixes --- .../Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php | 1 - app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php | 1 - app/code/Magento/MediaGalleryUi/composer.json | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php index c86f6970d2495..3244a7d14f0a3 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php @@ -8,7 +8,6 @@ class OnInsert extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { - /** * @var \Magento\Framework\Controller\Result\RawFactory */ diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index 2fc662a66e14c..b4c791822c59c 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -72,7 +72,6 @@ public function prepare(): void ); } - /** * Return allowed actions for media gallery */ diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index f4701306eb369..0f73fa5d21023 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -12,7 +12,8 @@ "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-synchronization-api": "*", "magento/module-media-content-api": "*", - "magento/module-cms": "*" + "magento/module-cms": "*", + "magento/module-media-gallery": "*", }, "type": "magento2-module", "license": [ From 8ef07f464a550f54d14428c438429a1df5597e1b Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Sat, 29 Aug 2020 00:09:57 +0300 Subject: [PATCH 41/96] fix css static tests --- .../MediaGalleryUi/view/adminhtml/web/css/source/_module.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less index a1f733849c5da..4b0d8f7dec89e 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -100,7 +100,7 @@ .media-gallery-container { .action-disabled { - opacity: 0.5; + opacity: .5; } .masonry-image-grid .no-data-message-container, .masonry-image-grid .error-message-container { From 3af84161abd031f4bb9054f1efbcc4439efc9be5 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Sat, 29 Aug 2020 11:21:00 +0300 Subject: [PATCH 42/96] Fix composer json --- app/code/Magento/MediaGalleryUi/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index 0f73fa5d21023..6008bb1579c49 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -13,7 +13,7 @@ "magento/module-media-gallery-synchronization-api": "*", "magento/module-media-content-api": "*", "magento/module-cms": "*", - "magento/module-media-gallery": "*", + "magento/module-media-gallery": "*" }, "type": "magento2-module", "license": [ From 7940f5952380c9f239b43940c6d607c9d2872177 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Sun, 30 Aug 2020 23:42:17 +0300 Subject: [PATCH 43/96] cover changes with mftf tests --- ...leryButtonNotDisabledOnPageActionGroup.xml | 25 ++++++ ...dminEnhancedMediaGalleryActionsSection.xml | 1 + .../AdminMediaGalleryCreateFolderAclTest.xml | 90 +++++++++++++++++++ .../AdminMediaGalleryDeleteAssetsAclTest.xml | 90 +++++++++++++++++++ .../AdminMediaGalleryDeleteFolderAclTest.xml | 90 +++++++++++++++++++ .../AdminMediaGalleryUploadAssetsAclTest.xml | 90 +++++++++++++++++++ .../Ui/Component/Control/UploadAssets.php | 1 - 7 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml new file mode 100644 index 0000000000000..af2b383143f62 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup"> + <annotations> + <description>Validates that the provided elemen present on page but have attribute disabled.</description> + </annotations> + <arguments> + <argument name="buttonName" type="string"/> + </arguments> + + <grabMultiple selector="{{AdminEnhancedMediaGalleryActionsSection.notDisabledButtons}}" stepKey="verifyDisabledAttribute"/> + + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">verifyDisabledAttribute</actualResult> + <expectedResult type="array">[{{buttonName}}]</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml index 7f9a5aefdf69c..907f2c3116800 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml @@ -12,6 +12,7 @@ <element name="deleteViewButton" type="button" selector="//div[@data-bind='afterRender: \$data.setToolbarNode']//input/following-sibling::div/button[@class='action-delete']"/> <element name="upload" type="input" selector="#image-uploader-input"/> <element name="cancel" type="button" selector="[data-ui-id='cancel-button']"/> + <element name="notDisabledButtons" type="button" selector="//div[@class='page-actions floating-header']/button[not(@disabled='disabled') and not(@id='cancel')]"/> <element name="createFolder" type="button" selector="[data-ui-id='create-folder-button']"/> <element name="deleteFolder" type="button" selector="[data-ui-id='delete-folder-button']"/> <element name="imageSrc" type="text" selector="//div[@class='masonry-image-column' and contains(@data-repeat-index, '0')]//img[contains(@src,'{{src}}')]" parameterized="true"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml new file mode 100644 index 0000000000000..d2c980cfc923f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml @@ -0,0 +1,90 @@ +<?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="AdminMediaGalleryCreateFolderAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery cretae folder functionality"/> + <description value="User manages ACL rules for Media Gallery cretae folder functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete Folder"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Insert Assets into the content"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Upload Assets"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete Assets"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Create Folder"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml new file mode 100644 index 0000000000000..971d08692676d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml @@ -0,0 +1,90 @@ +<?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="AdminMediaGalleryDeleteAssetsAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery delete assets functionality"/> + <description value="User manages ACL rules for Media Gallery delete assets functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Create Folder"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Insert Assets into the content"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Upload Assets"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete Folder"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Delete Images..."/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml new file mode 100644 index 0000000000000..22e834e9c5fd5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml @@ -0,0 +1,90 @@ +<?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="AdminMediaGalleryDeleteFolderAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery delete folder functionality"/> + <description value="User manages ACL rules for Media Gallery delete folder functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Create Folder"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Insert Assets into the content"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Upload Assets"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete Assets"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Delete Folder"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml new file mode 100644 index 0000000000000..1ddf2a5a23263 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml @@ -0,0 +1,90 @@ +<?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="AdminMediaGalleryUploadAssetsAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery upload assets functionality"/> + <description value="User manages ACL rules for Media Gallery upload assets functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Create Folder"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Insert Assets into the content"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete Assets"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete Folder"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Upload Image"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php index 3e7756d319c8a..b74e3ebebe5cc 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -38,7 +38,6 @@ public function getButtonData(): array { $buttonData = [ 'label' => __('Upload Image'), - 'disabled' => 'disabled', 'on_click' => 'jQuery("#image-uploader-input").click();', 'class' => 'action-default scalable add media-gallery-actions-buttons', 'sort_order' => 20, From e690a8d113389a05659c83b0bf75f1dcf49186b8 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 31 Aug 2020 09:25:07 +0300 Subject: [PATCH 44/96] Refactor mftf tests --- .../AdminMediaGalleryCreateFolderAclTest.xml | 25 +++--------------- .../AdminMediaGalleryDeleteAssetsAclTest.xml | 24 +++-------------- .../AdminMediaGalleryDeleteFolderAclTest.xml | 26 ++++--------------- .../AdminMediaGalleryUploadAssetsAclTest.xml | 24 +++-------------- 4 files changed, 17 insertions(+), 82 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml index d2c980cfc923f..8e1e4bbfbd48a 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml @@ -44,30 +44,13 @@ <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Media Gallery"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Folder"/> + <argument name="restrictedRole" value="Create Folder"/> </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Insert Assets into the content"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Upload Assets"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Assets"/> - </actionGroup> - - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> <argument name="User" value="adminRole"/> <argument name="restrictedRole" value="Pages"/> - </actionGroup> - <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> <argument name="role" value="adminRole"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml index 971d08692676d..bdfb5c475b3d8 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml @@ -42,32 +42,16 @@ <argument name="restrictedRole" value="Media Gallery"/> </actionGroup> <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Media Gallery"/> - </actionGroup> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Create Folder"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Insert Assets into the content"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Upload Assets"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Folder"/> + <argument name="restrictedRole" value="Delete Assets"/> </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> <argument name="User" value="adminRole"/> <argument name="restrictedRole" value="Pages"/> - </actionGroup> - <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> <argument name="role" value="adminRole"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml index 22e834e9c5fd5..f261e1bd4601c 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml @@ -44,30 +44,14 @@ <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Media Gallery"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Create Folder"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Insert Assets into the content"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Upload Assets"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Assets"/> + <argument name="restrictedRole" value="Delete Folder"/> </actionGroup> - - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> <argument name="User" value="adminRole"/> <argument name="restrictedRole" value="Pages"/> - </actionGroup> - <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> <argument name="role" value="adminRole"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml index 1ddf2a5a23263..4dae0b1ec5510 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml @@ -42,32 +42,16 @@ <argument name="restrictedRole" value="Media Gallery"/> </actionGroup> <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Media Gallery"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Create Folder"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckAddSelected"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Insert Assets into the content"/> - </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="UncheckUploadAssets"> - <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Assets"/> - </actionGroup> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Folder"/> + <argument name="restrictedRole" value="Upload Assets"/> </actionGroup> - <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> <argument name="User" value="adminRole"/> <argument name="restrictedRole" value="Pages"/> - </actionGroup> - <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> <argument name="role" value="adminRole"/> From a2346f4697a76f1d9905dc9968a79b57a9909e2b Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Mon, 31 Aug 2020 10:53:52 +0300 Subject: [PATCH 45/96] refactor --- app/code/Magento/Widget/Model/Widget.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 6d9bb1878708a..ec948063234da 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -313,7 +313,7 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) $directiveParams = ''; foreach ($params as $name => $value) { // Retrieve default option value if pre-configured - $directiveParams .= $this->getDirectiveParam($widget, $name, $value); + $directiveParams .= $value === null ? '' : $this->getDirectiveParam($widget, $name, $value); } $directive = sprintf('{{widget type="%s"%s%s}}', $type, $directiveParams, $this->getWidgetPageVarName($params)); @@ -354,9 +354,7 @@ private function getDirectiveParam(DataObject $widget, string $name, $value): st $value = $this->getPreparedValue($value); } - return $value !== null - ? sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)) - : ''; + return sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); } /** From 4c085d7504691c2ac458edb47a73f8b8cf16c4a2 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 31 Aug 2020 11:11:02 +0300 Subject: [PATCH 46/96] Fix flaky mftf test --- .../Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml index 3dd294fa50605..77ca32be269c2 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -36,7 +36,7 @@ <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminAdobeStockMediaGalleryClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> <argument name="title" value="ProductImage.filename"/> </actionGroup> From 0e28c9181301034653ac1dc8a26bc728a36fcc3b Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 31 Aug 2020 11:38:48 +0300 Subject: [PATCH 47/96] fix actiongroup to clear filters --- .../Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml index 77ca32be269c2..5a72c43b2f3ed 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -36,7 +36,7 @@ <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> - <actionGroup ref="AdminAdobeStockMediaGalleryClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersOnProductIndexPage"/> <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> <argument name="title" value="ProductImage.filename"/> </actionGroup> From cbf98debe1c1bc00f3ff54267ab5ea5f88ebe6b7 Mon Sep 17 00:00:00 2001 From: SmVladyslav <vlatame.tsg@gmail.com> Date: Mon, 31 Aug 2020 15:53:02 +0300 Subject: [PATCH 48/96] MC-36777: Incorrect conversion plane text to HTML --- .../Magento/Email/view/adminhtml/templates/template/edit.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index a16a3aae14b49..a377cd8ae6722 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -135,7 +135,7 @@ require([ content: "{$block->escapeJs(__('Are you sure you want to strip tags?'))}", actions: { confirm: function () { - this.unconvertedText = $('template_text').value; + self.unconvertedText = $('template_text').value; $('convert_button').hide(); $('template_text').value = $('template_text').value.stripScripts().replace( new RegExp('<style[^>]*>[\\S\\s]*?</style>', 'img'), '' From 000120d7d088900086d2cf07df355f1968ec119f Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Mon, 31 Aug 2020 23:10:14 +0800 Subject: [PATCH 49/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - added integration test for publisher --- .../Test/Integration/Model/PublisherTest.php | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php new file mode 100644 index 0000000000000..b96a6c58834f5 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Test\Integration\Model; + +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\MessageQueue\ConsumerInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronization\Model\Publish; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Framework\MessageQueue\ConsumerFactory; + +/** + * Test for media content Publisher + */ +class PublisherTest extends TestCase +{ + private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var Publish + */ + private $publish; + + /** + * @var ConsumerInterface + */ + private $consumer; + + /** + * @var ConsumerFactory + */ + private $consumerFactory; + + protected function setUp(): void + { + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->publish = Bootstrap::getObjectManager()->get(Publish::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->consumerFactory = Bootstrap::getObjectManager()->get(ConsumerFactory::class); + $this->consumer = Bootstrap::getObjectManager()->get(ConsumerInterface::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param array $contentIdentities + * @throws IntegrationException + * @throws LocalizedException + */ + public function testExecute(array $contentIdentities): void + { + // publish message to the queue + $this->publish->execute($contentIdentities); + + // run and process message + $consumerName = self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION; + $this->consumerFactory->get(self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION); + $this->consumer->process(); + + // verify synchronized media content + foreach ($contentIdentities as $contentIdentity) { + $assetId = 2020; + $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity); + var_dump($this->getAssetIds->execute($contentIdentityObject)); + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject)); + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + var_dump(count($synchronizedContentIdentities)); + $this->assertEquals(4, count($synchronizedContentIdentities)); + + $syncedIds = []; + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $syncedIds[] = $syncedContentIdentity->getEntityId(); + } + $this->assertContains($contentIdentity->getEntityId(), $syncedIds); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => 28767 + ], + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => 1567 + ], + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => 5 + ], + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => 1 + ] + ] + ] + ]; + } +} From 79151d69ba9ba8bffc919861f6a7aa650b04079e Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Tue, 1 Sep 2020 02:25:06 +0800 Subject: [PATCH 50/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated publisher integration test file --- .../Test/Integration/Model/PublisherTest.php | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php index b96a6c58834f5..69cb2853c7d11 100644 --- a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php @@ -9,14 +9,13 @@ use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\MessageQueue\ConsumerInterface; +use Magento\Framework\MessageQueue\ConsumerFactory; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; use Magento\MediaContentSynchronization\Model\Publish; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Magento\Framework\MessageQueue\ConsumerFactory; /** * Test for media content Publisher @@ -26,14 +25,14 @@ class PublisherTest extends TestCase private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; /** - * @var ContentIdentityInterfaceFactory + * @var ConsumerFactory */ - private $contentIdentityFactory; + private $consumerFactory; /** - * @var GetAssetIdsByContentIdentityInterface + * @var ContentIdentityInterfaceFactory */ - private $getAssetIds; + private $contentIdentityFactory; /** * @var GetContentByAssetIdsInterface @@ -41,28 +40,22 @@ class PublisherTest extends TestCase private $getContentIdentities; /** - * @var Publish - */ - private $publish; - - /** - * @var ConsumerInterface + * @var GetAssetIdsByContentIdentityInterface */ - private $consumer; + private $getAssetIds; /** - * @var ConsumerFactory + * @var Publish */ - private $consumerFactory; + private $publish; protected function setUp(): void { + $this->consumerFactory = Bootstrap::getObjectManager()->get(ConsumerFactory::class); $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); - $this->publish = Bootstrap::getObjectManager()->get(Publish::class); - $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); - $this->consumerFactory = Bootstrap::getObjectManager()->get(ConsumerFactory::class); - $this->consumer = Bootstrap::getObjectManager()->get(ConsumerInterface::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->publish = Bootstrap::getObjectManager()->get(Publish::class); } /** @@ -82,25 +75,26 @@ public function testExecute(array $contentIdentities): void $this->publish->execute($contentIdentities); // run and process message - $consumerName = self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION; - $this->consumerFactory->get(self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION); - $this->consumer->process(); + $batchSize = 1; + $maxNumberOfMessages = 1; + $consumer = $this->consumerFactory->get(self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, $batchSize); + $consumer->process($maxNumberOfMessages); // verify synchronized media content foreach ($contentIdentities as $contentIdentity) { $assetId = 2020; $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity); - var_dump($this->getAssetIds->execute($contentIdentityObject)); $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject)); $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); - var_dump(count($synchronizedContentIdentities)); $this->assertEquals(4, count($synchronizedContentIdentities)); $syncedIds = []; foreach ($synchronizedContentIdentities as $syncedContentIdentity) { $syncedIds[] = $syncedContentIdentity->getEntityId(); } - $this->assertContains($contentIdentity->getEntityId(), $syncedIds); + foreach ($contentIdentityObject as $identityObject) { + $this->assertContains($identityObject->getEntityId(), $syncedIds); + } } } From 80dae458e82ab8f55f502d5149bddfae8c722b99 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 31 Aug 2020 23:20:44 +0300 Subject: [PATCH 51/96] Fix flaky mftf tests --- ...diaGalleryAssertUsedInLinkPagesGridTest.xml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml index de8517eedae0e..4230640e76303 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml @@ -21,10 +21,6 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> - <after> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> - </after> - <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> <argument name="title" value="Unique page title MediaGalleryUi"/> @@ -37,9 +33,13 @@ <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> <argument name="image" value="ImageUpload3"/> </actionGroup> - <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> - <argument name="imageName" value="{{ImageMetadata.title}}"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> @@ -48,13 +48,11 @@ <argument name="entityName" value="Pages"/> </actionGroup> <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> - <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> </actionGroup> - <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> <argument name="urlKey" value="test-page-1"/> </actionGroup> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerfifyEmptyUsedIn"/> <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> @@ -62,7 +60,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> - <argument name="imageName" value="{{ImageMetadata.title}}"/> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> From 70042a6efc387c951860a98ede79246dc5beb053 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Tue, 1 Sep 2020 18:53:36 +0800 Subject: [PATCH 52/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated publisher integration test file --- .../Test/Integration/Model/PublisherTest.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php index 69cb2853c7d11..595c6d895bed0 100644 --- a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php @@ -86,7 +86,7 @@ public function testExecute(array $contentIdentities): void $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity); $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject)); $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); - $this->assertEquals(4, count($synchronizedContentIdentities)); + $this->assertEquals(2, count($synchronizedContentIdentities)); $syncedIds = []; foreach ($synchronizedContentIdentities as $syncedContentIdentity) { @@ -117,16 +117,6 @@ public function filesProvider(): array 'entityType' => 'catalog_product', 'field' => 'description', 'entityId' => 1567 - ], - [ - 'entityType' => 'cms_page', - 'field' => 'content', - 'entityId' => 5 - ], - [ - 'entityType' => 'cms_block', - 'field' => 'content', - 'entityId' => 1 ] ] ] From 798e61ffa7c242c9ab84b3b7ffd01a72623e71ed Mon Sep 17 00:00:00 2001 From: Olga Zakharchuk <olgaz@ven.com> Date: Wed, 2 Sep 2020 12:10:42 +0300 Subject: [PATCH 53/96] Fixed issue with critical error: The following tag(s) are not allowed: img Details: https://github.com/magento/magento2/pull/25418#discussion_r478456345 --- .../templates/order/creditmemo/items/renderer/default.phtml | 2 +- .../templates/order/invoice/items/renderer/default.phtml | 2 +- .../view/frontend/templates/order/items/renderer/default.phtml | 2 +- .../templates/order/shipment/items/renderer/default.phtml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml index b2e84691a45cf..c7a1b4b79b91a 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml @@ -17,7 +17,7 @@ <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml index 0176582f0fcd7..d05d6d01a5340 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml @@ -17,7 +17,7 @@ <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml index 51e43476238be..f0a0f46265a3e 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml @@ -17,7 +17,7 @@ $_item = $block->getItem(); <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml index 26fe74b0fc454..773100601d2b8 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml @@ -16,7 +16,7 @@ <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> From a48c133dd40bda9536512875173eb8b47e556d77 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 2 Sep 2020 14:20:28 +0300 Subject: [PATCH 54/96] MC-36615: [MFTF] AdminProductGridUrlFilterApplierTest fails because of bad design --- .../Mftf/Test/AdminProductGridUrlFilterApplierTest.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index 2eda7b8d02481..bfa80c2e24b48 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -20,8 +20,14 @@ </annotations> <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <!-- Should wait a bit for filters really cleared because waitForPageLoad does not wait for javascripts to be finished --> + <!-- Without this test will fail sometimes --> + <wait time="5" stepKey="waitFilterReallyCleared"/> + <reloadPage stepKey="reloadPage"/> </before> <after> From 67636e7e840923ddbe7cf58812a553239367b59e Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Wed, 2 Sep 2020 12:32:24 +0100 Subject: [PATCH 55/96] magento/magento2#29715: Moved ACL to MediaGalleryApi --- app/code/Magento/MediaGallery/etc/acl.xml | 26 ------------------- app/code/Magento/MediaGalleryApi/etc/acl.xml | 26 +++++++++++++++++++ .../Controller/Adminhtml/Category/Index.php | 2 +- .../MediaGalleryCatalogUi/composer.json | 1 - .../media_gallery_category_listing.xml | 2 +- .../Controller/Adminhtml/Asset/Search.php | 2 +- .../Adminhtml/Directories/Create.php | 2 +- .../Adminhtml/Directories/Delete.php | 2 +- .../Adminhtml/Directories/GetTree.php | 2 +- .../Controller/Adminhtml/Image/Delete.php | 2 +- .../Controller/Adminhtml/Image/Details.php | 2 +- .../Adminhtml/Image/SaveDetails.php | 2 +- .../Controller/Adminhtml/Image/Upload.php | 2 +- .../Controller/Adminhtml/Index/Index.php | 2 +- .../Controller/Adminhtml/Media/Index.php | 2 +- .../Ui/Component/Control/CreateFolder.php | 2 +- .../Ui/Component/Control/DeleteAssets.php | 2 +- .../Ui/Component/Control/DeleteFolder.php | 2 +- .../Ui/Component/Control/InsertAsstes.php | 2 +- .../Ui/Component/Control/UploadAssets.php | 2 +- .../Ui/Component/DirectoryTree.php | 2 +- .../Ui/Component/Listing/Columns/Url.php | 4 +-- .../Listing/Massactions/Massaction.php | 2 +- .../MediaGalleryUi/etc/adminhtml/menu.xml | 4 +-- .../layout/media_gallery_index_index.xml | 2 +- .../ui_component/media_gallery_listing.xml | 2 +- .../standalone_media_gallery_listing.xml | 2 +- 27 files changed, 52 insertions(+), 53 deletions(-) delete mode 100644 app/code/Magento/MediaGallery/etc/acl.xml create mode 100644 app/code/Magento/MediaGalleryApi/etc/acl.xml diff --git a/app/code/Magento/MediaGallery/etc/acl.xml b/app/code/Magento/MediaGallery/etc/acl.xml deleted file mode 100644 index a90d43e2a6cd2..0000000000000 --- a/app/code/Magento/MediaGallery/etc/acl.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?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:Acl/etc/acl.xsd"> - <acl> - <resources> - <resource id="Magento_Backend::admin"> - <resource id="Magento_Backend::content"> - <resource id="Magento_Backend::content_elements"> - <resource id="Magento_Cms::media_gallery" title="Media Gallery" translate="title"> - <resource id="Magento_MediaGallery::upload_assets" title="Upload Assets" translate="title" sortOrder="80"/> - <resource id="Magento_MediaGallery::delete_assets" title="Delete Assets" translate="title" sortOrder="70"/> - <resource id="Magento_MediaGallery::insert_assets" title="Insert Assets into the content" translate="title" sortOrder="60"/> - <resource id="Magento_MediaGallery::create_folder" title="Create Folder" translate="title" sortOrder="50"/> - <resource id="Magento_MediaGallery::delete_folder" title="Delete Folder" translate="title" sortOrder="40"/> - </resource> - </resource> - </resource> - </resource> - </resources> - </acl> -</config> diff --git a/app/code/Magento/MediaGalleryApi/etc/acl.xml b/app/code/Magento/MediaGalleryApi/etc/acl.xml new file mode 100644 index 0000000000000..50f7114d1f3be --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/etc/acl.xml @@ -0,0 +1,26 @@ +<?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:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Backend::content"> + <resource id="Magento_Backend::content_elements"> + <resource id="Magento_MediaGalleryApi::media_gallery" title="Media Gallery" translate="title"> + <resource id="Magento_MediaGalleryApi::upload_assets" title="Upload Assets" translate="title" sortOrder="80"/> + <resource id="Magento_MediaGalleryApi::delete_assets" title="Delete Assets" translate="title" sortOrder="70"/> + <resource id="Magento_MediaGalleryApi::insert_assets" title="Insert Assets into the content" translate="title" sortOrder="60"/> + <resource id="Magento_MediaGalleryApi::create_folder" title="Create Folder" translate="title" sortOrder="50"/> + <resource id="Magento_MediaGalleryApi::delete_folder" title="Delete Folder" translate="title" sortOrder="40"/> + </resource> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php index a541e9999b784..d2fb90f3bccff 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php @@ -18,7 +18,7 @@ */ class Index extends Action implements HttpGetActionInterface { - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * Get the media gallery layout diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json index 985d581beff25..418e113072ae3 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/composer.json +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -4,7 +4,6 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", - "magento/module-cms": "*", "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-store": "*", diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml index e12d90b95303b..e289c19fe6219 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -27,7 +27,7 @@ </storageConfig> <updateUrl path="mui/index/render"/> </settings> - <aclResource>Magento_Cms::media_gallery</aclResource> + <aclResource>Magento_MediaGalleryApi::media_gallery</aclResource> <dataProvider class="Magento\MediaGalleryCatalogUi\Model\Listing\DataProvider" name="media_gallery_category_listing_data_source"> <settings> <requestFieldName>entity_id</requestFieldName> diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php index 9b6c08edbc86d..6aaa7d44cb508 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -34,7 +34,7 @@ class Search extends Action implements HttpGetActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * @var SearchAssetsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php index f303cd4b7d5e5..f65f9d622aeba 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGallery::create_folder'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::create_folder'; /** * @var CreateDirectoriesByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php index f11fa36c0e499..bda891f93e907 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGallery::delete_folder'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::delete_folder'; /** * @var DeleteAssetsByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php index d4885cae055dd..0872685444193 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -25,7 +25,7 @@ class GetTree extends Action implements HttpGetActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * @var LoggerInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php index 30fc9a772cafd..0e91d85ed6a3a 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGallery::delete_assets'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::delete_assets'; /** * @var DeleteImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php index d959a070148ed..3929ad8268f4e 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php @@ -29,7 +29,7 @@ class Details extends Action implements HttpGetActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * @var GetDetailsByAssetId diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php index f41c489607b15..c4d2d3f07bed2 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -32,7 +32,7 @@ class SaveDetails extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * @var UpdateAsset diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php index 11df39bd2b1dc..902a253a78461 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGallery::upload_assets'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::upload_assets'; /** * @var UploadImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php index e97d93d86bb0d..4e0544a2870b9 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php @@ -18,7 +18,7 @@ */ class Index extends Action implements HttpGetActionInterface { - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * @var LayoutFactory diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php index 8c5b3d4d3a9ac..6b297a618e517 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -21,7 +21,7 @@ */ class Index extends Action implements HttpGetActionInterface { - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; /** * @var Config diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php index b05bf116e61c6..1d71fa9892275 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -13,7 +13,7 @@ */ class CreateFolder implements ButtonProviderInterface { - private const ACL_CREATE_FOLDER = 'Magento_MediaGallery::create_folder'; + private const ACL_CREATE_FOLDER = 'Magento_MediaGalleryApi::create_folder'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php index fd5564c44ff89..a2078513c0730 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -13,7 +13,7 @@ */ class DeleteAssets implements ButtonProviderInterface { - private const ACL_DELETE_ASSETS= 'Magento_MediaGallery::delete_assets'; + private const ACL_DELETE_ASSETS= 'Magento_MediaGalleryApi::delete_assets'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php index 2fa14f19b197d..3acafb2ae9c27 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -13,7 +13,7 @@ */ class DeleteFolder implements ButtonProviderInterface { - private const ACL_DELETE_FOLDER = 'Magento_MediaGallery::delete_folder'; + private const ACL_DELETE_FOLDER = 'Magento_MediaGalleryApi::delete_folder'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php index 4bd901da823fd..ee04e7e8a5837 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php @@ -13,7 +13,7 @@ */ class InsertAsstes implements ButtonProviderInterface { - private const ACL_INSERT_ASSETS = 'Magento_MediaGallery::insert_assets'; + private const ACL_INSERT_ASSETS = 'Magento_MediaGalleryApi::insert_assets'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php index b74e3ebebe5cc..440695a367305 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -13,7 +13,7 @@ */ class UploadAssets implements ButtonProviderInterface { - private const ACL_UPLOAD_ASSETS= 'Magento_MediaGallery::upload_assets'; + private const ACL_UPLOAD_ASSETS= 'Magento_MediaGalleryApi::upload_assets'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index b4c791822c59c..d42af8ef609ab 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -18,7 +18,7 @@ class DirectoryTree extends Container { private const ACL_IMAGE_ACTIONS = [ - 'delete_folder' => 'Magento_MediaGallery::delete_folder' + 'delete_folder' => 'Magento_MediaGalleryApi::delete_folder' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php index 7a6f59a75f1cf..ffbd726cd5f47 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -23,8 +23,8 @@ class Url extends Column { private const ACL_IMAGE_ACTIONS = [ - 'insert_assets' => 'Magento_MediaGallery::insert_assets', - 'delete_assets' => 'Magento_MediaGallery::delete_assets' + 'insert_assets' => 'Magento_MediaGalleryApi::insert_assets', + 'delete_assets' => 'Magento_MediaGalleryApi::delete_assets' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php index c16d90116bca0..e17048462c684 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php @@ -17,7 +17,7 @@ class Massaction extends Container { private const ACL_IMAGE_ACTIONS = [ - 'delete_assets' => 'Magento_MediaGallery::delete_assets' + 'delete_assets' => 'Magento_MediaGalleryApi::delete_assets' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml index 92839aa75ac8b..afa73373bd407 100644 --- a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> - <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_Cms::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> - <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_Cms::media_gallery"/> + <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_MediaGalleryApi::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> + <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_MediaGalleryApi::media_gallery"/> </menu> </config> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml index f41c0f91b2249..fa03c477ec9a2 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -11,7 +11,7 @@ <block name="media.gallery.container" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::container.phtml" - aclResource="Magento_Cms::media_gallery"> + aclResource="Magento_MediaGalleryApi::media_gallery"> <container name="gallery.actions" htmlTag="div" htmlClass="page-main-actions"> <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> </container> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index b7307f9a74fae..51c1dc21b016e 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -40,7 +40,7 @@ </storageConfig> <updateUrl path="mui/index/render"/> </settings> - <aclResource>Magento_Cms::media_gallery</aclResource> + <aclResource>Magento_MediaGalleryApi::media_gallery</aclResource> <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> <settings> <requestFieldName>id</requestFieldName> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index a53a46c61f75d..c1086ad891495 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -33,7 +33,7 @@ </storageConfig> <updateUrl path="mui/index/render"/> </settings> - <aclResource>Magento_Cms::media_gallery</aclResource> + <aclResource>Magento_MediaGalleryApi::media_gallery</aclResource> <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> <settings> <requestFieldName>id</requestFieldName> From b4d73e31d47067fe11bdfc8fa0dca84250849e83 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Thu, 3 Sep 2020 07:02:55 +0800 Subject: [PATCH 56/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated PR with requested changes --- .../Model/Consume.php | 28 ++++++++++--------- .../Test/Integration/Model/PublisherTest.php | 19 ++++++------- ...sCatalog.php => SynchronizeIdentities.php} | 2 +- .../SynchronizeIdentitiesCatalogTest.php | 17 +++++------ .../etc/di.xml | 4 +-- ...itiesCms.php => SynchronizeIdentities.php} | 2 +- .../SynchronizeIdentitiesCmsTest.php | 17 +++++------ .../MediaContentSynchronizationCms/etc/di.xml | 4 +-- 8 files changed, 48 insertions(+), 45 deletions(-) rename app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/{SynchronizeIdentitiesCatalog.php => SynchronizeIdentities.php} (95%) rename app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/{SynchronizeIdentitiesCms.php => SynchronizeIdentities.php} (97%) diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index 6aa8e6e831a25..b01c02cae4234 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -70,20 +70,22 @@ public function __construct( public function execute(OperationInterface $operation) : void { $identities = $this->serializer->unserialize($operation->getSerializedData()); - if (!empty($identities)) { - $contentIdentity = []; - foreach ($identities as $identity) { - $contentIdentity[] = $this->contentIdentityFactory->create( - [ - self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], - self::ENTITY_ID => $identity[self::ENTITY_ID], - self::FIELD => $identity[self::FIELD] - ] - ); - } - $this->synchronizeIdentities->execute($contentIdentity); - } else { + + if (empty($identities)) { $this->synchronize->execute(); + return; + } + + $contentIdentities = []; + foreach ($identities as $identity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], + self::ENTITY_ID => $identity[self::ENTITY_ID], + self::FIELD => $identity[self::FIELD] + ] + ); } + $this->synchronizeIdentities->execute($contentIdentities); } } diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php index 595c6d895bed0..2314796481b55 100644 --- a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php @@ -81,20 +81,19 @@ public function testExecute(array $contentIdentities): void $consumer->process($maxNumberOfMessages); // verify synchronized media content + $assetId = 2020; + $entityIds = []; foreach ($contentIdentities as $contentIdentity) { - $assetId = 2020; $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity); $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject)); - $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); - $this->assertEquals(2, count($synchronizedContentIdentities)); + $entityIds[] = $contentIdentityObject->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); - $syncedIds = []; - foreach ($synchronizedContentIdentities as $syncedContentIdentity) { - $syncedIds[] = $syncedContentIdentity->getEntityId(); - } - foreach ($contentIdentityObject as $identityObject) { - $this->assertContains($identityObject->getEntityId(), $syncedIds); - } + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); } } diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php similarity index 95% rename from app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php rename to app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php index a57b8eb2041cb..77188b65a8b88 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentitiesCatalog.php +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php @@ -11,7 +11,7 @@ use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -class SynchronizeIdentitiesCatalog implements SynchronizeIdentitiesInterface +class SynchronizeIdentities implements SynchronizeIdentitiesInterface { private const FIELD_CATALOG_PRODUCT = 'catalog_product'; private const FIELD_CATALOG_CATEGORY = 'catalog_category'; diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php index ae4bb77881834..4a1ba1445ffe5 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php @@ -84,17 +84,18 @@ public function testExecute(array $mediaContentIdentities): void $this->assertNotEmpty($contentIdentities); $this->synchronizeIdentities->execute($contentIdentities); + $assetId = 2020; + $entityIds = []; foreach ($contentIdentities as $contentIdentity) { - $assetId = 2020; $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); - $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); - $this->assertEquals(2, count($synchronizedContentIdentities)); + $entityIds[] = $contentIdentity->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); - $syncedIds = []; - foreach ($synchronizedContentIdentities as $syncedContentIdentity) { - $syncedIds[] = $syncedContentIdentity->getEntityId(); - } - $this->assertContains($contentIdentity->getEntityId(), $syncedIds); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); } } diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml index 7c391faf3f955..d26ed06a42586 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -41,8 +41,8 @@ <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool"> <arguments> <argument name="synchronizers" xsi:type="array"> - <item name="media_content_catalog_synchronizeIdentity" - xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\SynchronizeIdentitiesCatalog + <item name="media_content_catalog" + xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\SynchronizeIdentities </item> </argument> </arguments> diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php similarity index 97% rename from app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php rename to app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php index 5685e284e20c3..7dd2596a910de 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentitiesCms.php +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php @@ -12,7 +12,7 @@ use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -class SynchronizeIdentitiesCms implements SynchronizeIdentitiesInterface +class SynchronizeIdentities implements SynchronizeIdentitiesInterface { private const FIELD_CMS_PAGE = 'cms_page'; private const FIELD_CMS_BLOCK = 'cms_block'; diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php index 48841ecf24658..bdd8bfd1105d3 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php @@ -84,17 +84,18 @@ public function testExecute(array $mediaContentIdentities): void $this->assertNotEmpty($contentIdentities); $this->synchronizeIdentities->execute($contentIdentities); + $assetId = 2020; + $entityIds = []; foreach ($contentIdentities as $contentIdentity) { - $assetId = 2020; $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); - $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); - $this->assertEquals(2, count($synchronizedContentIdentities)); + $entityIds[] = $contentIdentity->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); - $syncedIds = []; - foreach ($synchronizedContentIdentities as $syncedContentIdentity) { - $syncedIds[] = $syncedContentIdentity->getEntityId(); - } - $this->assertContains($contentIdentity->getEntityId(), $syncedIds); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); } } diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml index 43a863dc95764..94e9c686aafbb 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -17,8 +17,8 @@ <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool"> <arguments> <argument name="synchronizers" xsi:type="array"> - <item name="media_content_cms_synchronizeIdentity" - xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\SynchronizeIdentitiesCms + <item name="media_content_cms" + xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\SynchronizeIdentities </item> </argument> </arguments> From 0591d51dbbff7bfb4714a29d8b2e682d46f76708 Mon Sep 17 00:00:00 2001 From: saphaljha <saphal.jha@krishtechnolabs.com> Date: Thu, 3 Sep 2020 14:32:00 +0300 Subject: [PATCH 57/96] covered mftf for bundle items --- ...aceOrderWithMultipleOptionsSuccessTest.xml | 93 +++++++++++++++++++ .../Mftf/Section/AdminInvoiceItemsSection.xml | 1 + 2 files changed, 94 insertions(+) create mode 100644 app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml new file mode 100644 index 0000000000000..4e01f950cd087 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml @@ -0,0 +1,93 @@ +<?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="StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product details page"/> + <title value="Customer should be able to see all the bundle items in invoice view"/> + <description value="Customer should be able to see all the bundle items in invoice view"/> + <severity value="CRITICAL"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="firstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProduct"/> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="firstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="secondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Create new bundle product --> + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createBundleProduct"> + <argument name="productType" value="bundle"/> + </actionGroup> + + <!-- Fill all main fields --> + <actionGroup ref="FillMainBundleProductFormActionGroup" stepKey="fillMainProductFields"/> + + <!-- Add first bundle option to the product --> + <actionGroup ref="AddBundleOptionWithTwoProductsActionGroup" stepKey="addFirstBundleOption"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$firstSimpleProduct.sku$"/> + <argument name="prodTwoSku" value="$secondSimpleProduct.sku$$"/> + <argument name="optionTitle" value="{{CheckboxOption.title}}"/> + <argument name="inputType" value="{{CheckboxOption.type}}"/> + </actionGroup> + + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveWithThreeOptions"/> + + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> + <argument name="productUrl" value="{{BundleProduct.name}}"/> + </actionGroup> + + <!-- Add bundle to cart --> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> + <argument name="productUrl" value="{{BundleProduct.name}}"/> + </actionGroup> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts(CheckboxOption.title, '1')}}" stepKey="selectOption2Product1"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts(CheckboxOption.title, '2')}}" stepKey="selectOption2Product2"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + + <!--Navigate to checkout--> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> + <!-- Click next button to open payment section --> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Click place order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Order review page has address that was created during checkout --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + + <!-- Open create invoice page --> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> + + <!-- Assert item options display --> + <see selector="{{AdminInvoiceItemsSection.bundleItem}}" userInput="50 x $firstSimpleProduct.sku$" stepKey="seeFirstProductInList"/> + <see selector="{{AdminInvoiceItemsSection.bundleItem}}" userInput="50 x $secondSimpleProduct.sku$" stepKey="seeSecondProductInList"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml index 92c01cf380746..4d75589c40e9c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml @@ -28,5 +28,6 @@ <element name="discountAmountColumn" type="text" selector=".order-invoice-tables .col-discount .price"/> <element name="totalColumn" type="text" selector=".order-invoice-tables .col-total .price"/> <element name="updateQty" type="button" selector=".order-invoice-tables tfoot button[data-ui-id='order-items-update-button']"/> + <element name="bundleItem" type="text" selector="#invoice_item_container .option-value"/> </section> </sections> From 140c351ead8abf866df95312985b871bfd60ec55 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Thu, 3 Sep 2020 14:32:20 +0300 Subject: [PATCH 58/96] fix static --- .../templates/sales/invoice/create/items/renderer.phtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index 9564fbb785119..15ef3c311e396 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -130,8 +130,8 @@ $catalogHelper = $block->getData('catalogHelper'); <?php endif; ?> </td> <td class="col-qty-invoice"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> - <?php if ($block->canEditQty() && $canEditItemQty) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> + <?php if ($block->canEditQty() && $canEditItemQty): ?> <input type="text" class="input-text admin__control-text qty-input" name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" From a55b88012ad40be7bcc64bda42b52e67e0c0b073 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 3 Sep 2020 14:23:56 +0100 Subject: [PATCH 59/96] Introduced MediaGalleryRenditions and MediaGalleryRenditionsApi modules --- .../MediaGalleryRenditions/LICENSE.txt | 48 ++++ .../MediaGalleryRenditions/LICENSE_AFL.txt | 48 ++++ .../MediaGalleryRenditions/Model/Config.php | 117 ++++++++++ .../Model/GenerateRenditions.php | 216 ++++++++++++++++++ .../Model/GetRenditionPath.php | 26 +++ .../Queue/FetchRenditionPathsBatches.php | 111 +++++++++ .../Model/Queue/GetFilesIterator.php | 33 +++ .../Model/Queue/ScheduleRenditionsUpdate.php | 44 ++++ .../Model/Queue/UpdateRenditions.php | 126 ++++++++++ .../Plugin/RemoveRenditions.php | 84 +++++++ .../Plugin/SetRenditionPath.php | 111 +++++++++ .../Plugin/UpdateRenditionsOnConfigChange.php | 62 +++++ .../Magento/MediaGalleryRenditions/README.md | 13 ++ ...ractAssetsFromContentWithRenditionTest.php | 116 ++++++++++ .../Model/GenerateRenditionsTest.php | 124 ++++++++++ .../Model/GetRenditionPathTest.php | 77 +++++++ .../Test/_files/magento_large_image.jpg | Bin 0 -> 55303 bytes .../Test/_files/magento_medium_image.jpg | Bin 0 -> 34986 bytes .../MediaGalleryRenditions/composer.json | 24 ++ .../etc/adminhtml/system.xml | 24 ++ .../etc/communication.xml | 14 ++ .../MediaGalleryRenditions/etc/config.xml | 17 ++ .../Magento/MediaGalleryRenditions/etc/di.xml | 31 +++ .../etc/media_content.xml | 18 ++ .../MediaGalleryRenditions/etc/module.xml | 10 + .../etc/queue_consumer.xml | 11 + .../etc/queue_publisher.xml | 12 + .../etc/queue_topology.xml | 14 ++ .../MediaGalleryRenditions/registration.php | 14 ++ .../Api/GenerateRenditionsInterface.php | 21 ++ .../Api/GetRenditionPathInterface.php | 22 ++ .../MediaGalleryRenditionsApi/LICENSE.txt | 48 ++++ .../MediaGalleryRenditionsApi/LICENSE_AFL.txt | 48 ++++ .../MediaGalleryRenditionsApi/README.md | 13 ++ .../MediaGalleryRenditionsApi/composer.json | 21 ++ .../MediaGalleryRenditionsApi/etc/module.xml | 10 + .../registration.php | 14 ++ composer.json | 2 + composer.lock | 2 +- 39 files changed, 1745 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/Config.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php create mode 100644 app/code/Magento/MediaGalleryRenditions/README.md create mode 100644 app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php create mode 100644 app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg create mode 100644 app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg create mode 100644 app/code/Magento/MediaGalleryRenditions/composer.json create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/communication.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/config.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/di.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/media_content.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/registration.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/README.md create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/composer.json create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/registration.php diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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. diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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/MediaGalleryRenditions/Model/Config.php b/app/code/Magento/MediaGalleryRenditions/Model/Config.php new file mode 100644 index 0000000000000..a40fbb41bd831 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Config.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Class responsible for providing access to Media Gallery Renditions system configuration. + */ +class Config +{ + private const TABLE_CORE_CONFIG_DATA = 'core_config_data'; + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + ResourceConnection $resourceConnection + ) { + $this->scopeConfig = $scopeConfig; + $this->resourceConnection = $resourceConnection; + } + + /** + * Check if the media gallery is enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } + + /** + * Get max width + * + * @return int + */ + public function getWidth(): int + { + try { + return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } catch (NoSuchEntityException $exception) { + return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } + } + + /** + * Get max height + * + * @return int + */ + public function getHeight(): int + { + try { + return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } catch (NoSuchEntityException $exception) { + return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } + } + + /** + * Get value from database bypassing config cache + * + * @param string $path + * @return int + * @throws NoSuchEntityException + */ + private function getDatabaseValue(string $path): int + { + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from( + [ + 'config' => $this->resourceConnection->getTableName(self::TABLE_CORE_CONFIG_DATA) + ], + [ + 'value' + ] + ) + ->where('config.path = ?', $path); + $value = $connection->query($select)->fetchColumn(); + + if ($value === false) { + throw new NoSuchEntityException( + __( + 'The config value for %path is not saved to database.', + ['path' => $path] + ) + ); + } + + return (int) $value; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php new file mode 100644 index 0000000000000..d1bfcb82a6a84 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php @@ -0,0 +1,216 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Image\AdapterFactory; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +class GenerateRenditions implements GenerateRenditionsInterface +{ + private const IMAGE_FILE_NAME_PATTERN = '#\.(jpg|jpeg|gif|png)$# i'; + + /** + * @var AdapterFactory + */ + private $imageFactory; + + /** + * @var Config + */ + private $config; + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param AdapterFactory $imageFactory + * @param Config $config + * @param GetRenditionPathInterface $getRenditionPath + * @param Filesystem $filesystem + * @param File $driver + */ + public function __construct( + AdapterFactory $imageFactory, + Config $config, + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + File $driver, + IsPathExcludedInterface $isPathExcluded, + LoggerInterface $log + ) { + $this->imageFactory = $imageFactory; + $this->config = $config; + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->isPathExcluded = $isPathExcluded; + $this->log = $log; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $failedPaths = []; + + foreach ($paths as $path) { + try { + $this->generateRendition($path); + } catch (\Exception $exception) { + $this->log->error($exception); + $failedPaths[] = $path; + } + } + + if (!empty($failedPaths)) { + throw new LocalizedException( + __( + 'Cannot create rendition for media asset paths: %paths', + [ + 'paths' => implode(', ', $failedPaths) + ] + ) + ); + } + } + + /** + * Generate rendition for media asset path + * + * @param string $path + * @throws FileSystemException + * @throws LocalizedException + * @throws \Exception + */ + private function generateRendition(string $path): void + { + $renditionPath = $this->getRenditionPath->execute($path); + $this->createDirectory($renditionPath); + + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + + if ($this->shouldFileBeResized($absolutePath)) { + $this->createResizedRendition( + $absolutePath, + $this->getMediaDirectory()->getAbsolutePath($renditionPath) + ); + } else { + $this->getMediaDirectory()->copyFile($path, $renditionPath); + } + } + + /** + * @param string $path + * @throws FileSystemException + * @throws LocalizedException + */ + private function validateAsset(string $path): void + { + if (!$this->getMediaDirectory()->isFile($path)) { + throw new LocalizedException(__('Media asset file %path does not exist!', ['path' => $path])); + } + + if ($this->isPathExcluded->execute($path)) { + throw new LocalizedException( + __('Could not create rendition for image, path is restricted: %path', ['path' => $path]) + ); + } + + if (!preg_match(self::IMAGE_FILE_NAME_PATTERN, $path)) { + throw new LocalizedException( + __('Could not create rendition for image, unsupported file type: %path.', ['path' => $path]) + ); + } + } + + /** + * Create directory for rendition file + * + * @param string $path + * @throws LocalizedException + */ + private function createDirectory(string $path): void + { + try { + $this->getMediaDirectory()->create($this->driver->getParentDirectory($path)); + } catch (\Exception $exception) { + throw new LocalizedException(__('Cannot create directory for rendition %path', ['path' => $path])); + } + } + + /** + * Create rendition file + * + * @param string $absolutePath + * @param string $absoluteRenditionPath + * @throws \Exception + */ + private function createResizedRendition(string $absolutePath, string $absoluteRenditionPath): void + { + $image = $this->imageFactory->create(); + $image->open($absolutePath); + $image->keepAspectRatio(true); + $image->resize($this->config->getWidth(), $this->config->getHeight()); + $image->save($absoluteRenditionPath); + } + + /** + * Check if image needs to resize or not + * + * @param string $absolutePath + * @return bool + */ + private function shouldFileBeResized(string $absolutePath): bool + { + [$width, $height] = getimagesize($absolutePath); + return $width > $this->config->getWidth() || $height > $this->config->getHeight(); + } + + /** + * Retrieve a media directory instance with write permissions + * + * @return WriteInterface + * @throws FileSystemException + */ + private function getMediaDirectory(): WriteInterface + { + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php new file mode 100644 index 0000000000000..1c93141429ab0 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; + +class GetRenditionPath implements GetRenditionPathInterface +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * Returns Rendition image path + * + * @param string $path + * @return string + */ + public function execute(string $path): string + { + return self::RENDITIONS_DIRECTORY_NAME . '/' . ltrim($path, '/'); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php new file mode 100644 index 0000000000000..9cec7edbbd39a --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Psr\Log\LoggerInterface; + +/** + * Fetch files from media storage in batches + */ +class FetchRenditionPathsBatches +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * @var GetFilesIterator + */ + private $getFilesIterator; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $fileExtensions; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var int + */ + private $batchSize; + + /** + * @param LoggerInterface $log + * @param Filesystem $filesystem + * @param GetFilesIterator $assetsIterator + * @param int $batchSize + * @param array $fileExtensions + */ + public function __construct( + LoggerInterface $log, + Filesystem $filesystem, + GetFilesIterator $getFilesIterator, + int $batchSize, + array $fileExtensions + ) { + $this->log = $log; + $this->getFilesIterator = $getFilesIterator; + $this->filesystem = $filesystem; + $this->batchSize = $batchSize; + $this->fileExtensions = $fileExtensions; + } + + /** + * Return files from files system by provided size of batch + */ + public function execute(): \Traversable + { + $index = 0; + $batch = []; + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $iterator = $this->getFilesIterator->execute( + $mediaDirectory->getAbsolutePath(self::RENDITIONS_DIRECTORY_NAME) + ); + + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + $relativePath = $mediaDirectory->getRelativePath($file->getPathName()); + if (!$this->isApplicable($relativePath)) { + continue; + } + + $batch[] = $relativePath; + if (++$index == $this->batchSize) { + yield $batch; + $index = 0; + $batch = []; + } + } + if (count($batch) > 0) { + yield $batch; + } + } + + /** + * Is the path a valid image path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php new file mode 100644 index 0000000000000..97efcdc81ba50 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +/** + * Retrieve files iterator for path + */ +class GetFilesIterator +{ + /** + * Get files iterator for provided path + * + * @param string $path + * @return \RecursiveIteratorIterator + */ + public function execute(string $path): \RecursiveIteratorIterator + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $path, + \FilesystemIterator::SKIP_DOTS | + \FilesystemIterator::UNIX_PATHS | + \RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php new file mode 100644 index 0000000000000..051c883025587 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media gallery renditions update message to the queue. + */ +class ScheduleRenditionsUpdate +{ + private const TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS = 'media.gallery.renditions.update'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media gallery renditions update message to the queue. + * + * @param array $paths + */ + public function execute(array $paths = []): void + { + $this->publisher->publish( + self::TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS, + $paths + ); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php new file mode 100644 index 0000000000000..1fcc28d235f09 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Psr\Log\LoggerInterface; + +/** + * Renditions update queue consumer. + */ +class UpdateRenditions +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var FetchRenditionPathsBatches + */ + private $fetchRenditionPathsBatches; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GenerateRenditionsInterface $generateRenditions + * @param FetchRenditionPathsBatches $fetchRenditionPathsBatches + * @param LoggerInterface $log + */ + public function __construct( + GenerateRenditionsInterface $generateRenditions, + FetchRenditionPathsBatches $fetchRenditionPathsBatches, + LoggerInterface $log + ) { + $this->generateRenditions = $generateRenditions; + $this->fetchRenditionPathsBatches = $fetchRenditionPathsBatches; + $this->log = $log; + } + + /** + * Update renditions for given paths, if empty array is provided - all renditions are updated + * + * @param array $paths + * @throws LocalizedException + */ + public function execute(array $paths): void + { + if (!empty($paths)) { + $this->updateRenditions($paths); + return; + } + + foreach ($this->fetchRenditionPathsBatches->execute() as $renditionPaths) { + $this->updateRenditions($renditionPaths); + } + } + + /** + * Update renditions and log exceptions + * + * @param string[] $paths + */ + private function updateRenditions(array $renditionPaths): void + { + try { + $this->generateRenditions->execute($this->getAssetPaths($renditionPaths)); + } catch (LocalizedException $exception) { + $this->log->error($exception); + } + } + + /** + * Get asset paths based on rendition paths + * + * @param string[] $renditionPaths + * @return string[] + */ + private function getAssetPaths(array $renditionPaths): array + { + $paths = []; + + foreach ($renditionPaths as $renditionPath) { + try { + $paths[] = $this->getAssetPath($renditionPath); + } catch (\Exception $exception) { + $this->log->error($exception); + } + } + + return $paths; + } + + /** + * Get asset path based on rendition path + * + * @param string $renditionPath + * @return string + * @throws LocalizedException + */ + private function getAssetPath(string $renditionPath): string + { + if (strpos($renditionPath, self::RENDITIONS_DIRECTORY_NAME) !== 0) { + throw new LocalizedException( + __( + 'Incorrect rendition path provided for update: %path', + [ + 'path' => $renditionPath + ] + ) + ); + } + + return substr($renditionPath, strlen(self::RENDITIONS_DIRECTORY_NAME)); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php new file mode 100644 index 0000000000000..dd75800e4384d --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Remove renditions when assets are removed + */ +class RemoveRenditions +{ + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GetRenditionPathInterface $getRenditionPath + * @param Filesystem $filesystem + * @param LoggerInterface $log + */ + public function __construct( + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + LoggerInterface $log + ) { + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->log = $log; + } + + /** + * Remove renditions when assets are removed + * + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param \Closure $proceed + * @param array $paths + */ + public function aroundExecute( + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + \Closure $proceed, + array $paths + ): void { + $proceed($paths); + $this->removeRenditions($paths); + } + + /** + * Remove rendition files + * + * @param array $paths + */ + private function removeRenditions(array $paths): void + { + foreach ($paths as $path) { + try { + $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->delete( + $this->getRenditionPath->execute($path) + ); + } catch (\Exception $exception) { + $this->log->error($exception); + } + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php new file mode 100644 index 0000000000000..ec2012c528ef1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryRenditions\Model\Config; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Intercept and set renditions path on PrepareImage + */ +class SetRenditionPath +{ + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var Images + */ + private $imagesHelper; + + /** + * @var Config + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GetRenditionPathInterface $getRenditionPath + * @param GenerateRenditionsInterface $generateRenditions + * @param Images $imagesHelper + * @param Config $config + * @param LoggerInterface $log + */ + public function __construct( + GetRenditionPathInterface $getRenditionPath, + GenerateRenditionsInterface $generateRenditions, + Images $imagesHelper, + Config $config, + LoggerInterface $log + ) { + $this->getRenditionPath = $getRenditionPath; + $this->generateRenditions = $generateRenditions; + $this->imagesHelper = $imagesHelper; + $this->config = $config; + $this->log = $log; + } + + /** + * Replace the original asset path with rendition path + * + * @param GetInsertImageContent $subject + * @param string $encodedFilename + * @param bool $forceStaticPath + * @param bool $renderAsTag + * @param int|null $storeId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute( + GetInsertImageContent $subject, + string $encodedFilename, + bool $forceStaticPath, + bool $renderAsTag, + ?int $storeId = null + ): array { + $arguments = [ + $encodedFilename, + $forceStaticPath, + $renderAsTag, + $storeId + ]; + + if (!$this->config->isEnabled()) { + return $arguments; + } + + $path = $this->imagesHelper->idDecode($encodedFilename); + + try { + $this->generateRenditions->execute([$path]); + } catch (LocalizedException $exception) { + $this->log->error($exception); + return $arguments; + } + + $arguments[0] = $this->imagesHelper->idEncode($this->getRenditionPath->execute($path)); + + return $arguments; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php new file mode 100644 index 0000000000000..9feb63377add9 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Framework\App\Config\Value; +use Magento\MediaGalleryRenditions\Model\Queue\ScheduleRenditionsUpdate; + +/** + * Update renditions if corresponding configuration changes + */ +class UpdateRenditionsOnConfigChange +{ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScheduleRenditionsUpdate + */ + private $scheduleRenditionsUpdate; + + /** + * @param ScheduleRenditionsUpdate $scheduleRenditionsUpdate + */ + public function __construct(ScheduleRenditionsUpdate $scheduleRenditionsUpdate) + { + $this->scheduleRenditionsUpdate = $scheduleRenditionsUpdate; + } + + /** + * Update renditions when configuration is changed + * + * @param Value $config + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Value $config, Value $result): Value + { + if ($this->isRenditionsValue($result) && $result->isValueChanged()) { + $this->scheduleRenditionsUpdate->execute(); + } + + return $result; + } + + /** + * Does configuration value relates to renditions + * + * @param Value $value + * @return bool + */ + private function isRenditionsValue(Value $value) + { + return $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH + || $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md new file mode 100644 index 0000000000000..df856e8003a84 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditions module + +The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php new file mode 100644 index 0000000000000..05bb01b9ff433 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for Extracting assets from rendition paths/urls in content + */ +class ExtractAssetsFromContentWithRenditionTest extends TestCase +{ + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractAssetsFromContent = Bootstrap::getObjectManager() + ->get(ExtractAssetsFromContentInterface::class); + } + + /** + * Assert rendition urls/path in the content are associated with an asset + * + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * + * @dataProvider contentProvider + * @param string $content + * @param array $assetIds + */ + public function testExecute(string $content, array $assetIds): void + { + $assets = $this->extractAssetsFromContent->execute($content); + + $extractedAssetIds = []; + foreach ($assets as $asset) { + $extractedAssetIds[] = $asset->getId(); + } + + sort($assetIds); + sort($extractedAssetIds); + + $this->assertEquals($assetIds, $extractedAssetIds); + } + + /** + * Data provider for testExecute + * + * @return array + */ + public function contentProvider() + { + return [ + 'Empty Content' => [ + '', + [] + ], + 'No paths in content' => [ + 'content without paths', + [] + ], + 'Relevant rendition path in content' => [ + 'content {{media url=".renditions/testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant wysiwyg rendition path in content' => [ + 'content <img src="https://domain.com/media/.renditions/testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant rendition path content with pub' => [ + '/pub/media/.renditions/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Relevant rendition path content' => [ + '/media/.renditions/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Relevant existing media paths w/o rendition in content' => [ + 'content {{media url="testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant existing paths w/o rendition in content with pub' => [ + '/pub/media/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Non-existing rendition paths in content' => [ + 'content {{media url=".renditions/non-existing-path.png"}} content', + [] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php new file mode 100644 index 0000000000000..9655f3949d404 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\MediaGalleryRenditions\Model\Config; +use PHPUnit\Framework\TestCase; + +class GenerateRenditionsTest extends TestCase +{ + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var Config + */ + private $renditionSizeConfig; + + /** + * @var DriverInterface + */ + private $driver; + + protected function setup(): void + { + $this->generateRenditions = Bootstrap::getObjectManager()->get(GenerateRenditionsInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->renditionSizeConfig = Bootstrap::getObjectManager()->get(Config::class); + } + + public static function tearDownAfterClass(): void + { + /** @var WriteInterface $mediaDirectory */ + $mediaDirectory = Bootstrap::getObjectManager()->get( + Filesystem::class + )->getDirectoryWrite( + DirectoryList::MEDIA + ); + if ($mediaDirectory->isExist($mediaDirectory->getAbsolutePath() . '/.renditions')) { + $mediaDirectory->delete($mediaDirectory->getAbsolutePath() . '/.renditions'); + } + } + + /** + * @dataProvider renditionsImageProvider + * + * Test for generation of rendition images. + * + * @param string $path + * @param string $renditionPath + * @throws LocalizedException + */ + public function testExecute(string $path, string $renditionPath): void + { + $this->copyImage($path); + $this->generateRenditions->execute([$path]); + $expectedRenditionPath = $this->mediaDirectory->getAbsolutePath($renditionPath); + list($imageWidth, $imageHeight) = getimagesize($expectedRenditionPath); + $this->assertFileExists($expectedRenditionPath); + $this->assertLessThanOrEqual( + $this->renditionSizeConfig->getWidth(), + $imageWidth, + 'Generated renditions image width should be less than or equal to configured value' + ); + $this->assertLessThanOrEqual( + $this->renditionSizeConfig->getHeight(), + $imageHeight, + 'Generated renditions image height should be less than or equal to configured value' + ); + } + + /** + * @param array $paths + * @throws FileSystemException + */ + private function copyImage(string $path): void + { + $imagePath = realpath(__DIR__ . '/../../_files' . $path); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + } + + /** + * @return array + */ + public function renditionsImageProvider(): array + { + return [ + 'rendition_image_not_generated' => [ + 'paths' => '/magento_medium_image.jpg', + 'renditionPath' => ".renditions/magento_medium_image.jpg" + ], + 'rendition_image_generated' => [ + 'paths' => '/magento_large_image.jpg', + 'renditionPath' => ".renditions/magento_large_image.jpg" + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php new file mode 100644 index 0000000000000..0f8b61147986c --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class GetRenditionPathTest extends TestCase +{ + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + protected function setup(): void + { + $this->getRenditionPath = Bootstrap::getObjectManager()->get(GetRenditionPathInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + } + + /** + * @dataProvider getImageProvider + * + * Test for getting a rendition path. + */ + public function testExecute(string $path, string $expectedRenditionPath): void + { + $imagePath = realpath(__DIR__ . '/../../_files' . $path); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + $this->assertEquals($expectedRenditionPath, $this->getRenditionPath->execute($path)); + } + + /** + * @return array + */ + public function getImageProvider(): array + { + return [ + 'return_original_path' => [ + 'path' => '/magento_medium_image.jpg', + 'expectedRenditionPath' => '.renditions/magento_medium_image.jpg' + ], + 'return_rendition_path' => [ + 'path' => '/magento_large_image.jpg', + 'expectedRenditionPath' => '.renditions/magento_large_image.jpg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c377daf8fb0b390d89aa4f6f111715fc9118ca1b GIT binary patch literal 55303 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 Hh5vs5{BvNq literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6dc8cd69e41c1ed0fcdbd38370c3b5b90d6c3ded GIT binary patch literal 34986 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 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json new file mode 100644 index 0000000000000..948f6ce3b14f3 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery-renditions", + "description": "Magento module that implements height and width fields for for media gallery items.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-renditions-api": "*", + "magento/module-media-content-api": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditions\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..64f338d53a283 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml @@ -0,0 +1,24 @@ +<?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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery_renditions" translate="label" type="text" sortOrder="1010" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Media Gallery Renditions</label> + <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Max Width</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Max Height</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/communication.xml b/app/code/Magento/MediaGalleryRenditions/etc/communication.xml new file mode 100644 index 0000000000000..2c343c4f8086a --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/communication.xml @@ -0,0 +1,14 @@ +<?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:Communication/etc/communication.xsd"> + <topic name="media.gallery.renditions.update" is_synchronous="false" request="string[]"> + <handler name="media.gallery.renditions.update.handler" + type="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml new file mode 100644 index 0000000000000..58c5aa1f11fd2 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -0,0 +1,17 @@ +<?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:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery_renditions> + <width>1000</width> + <height>1000</height> + </media_gallery_renditions> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/di.xml b/app/code/Magento/MediaGalleryRenditions/etc/di.xml new file mode 100644 index 0000000000000..af53810b7f69e --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/di.xml @@ -0,0 +1,31 @@ +<?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\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface" type="Magento\MediaGalleryRenditions\Model\GenerateRenditions"/> + <preference for="Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface" type="Magento\MediaGalleryRenditions\Model\GetRenditionPath"/> + <type name="Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent"> + <plugin name="set_rendition_path" type="Magento\MediaGalleryRenditions\Plugin\SetRenditionPath"/> + </type> + <type name="Magento\MediaGalleryRenditions\Model\Queue\FetchRenditionPathsBatches"> + <arguments> + <argument name="batchSize" xsi:type="number">100</argument> + <argument name="fileExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Config\Value"> + <plugin name="admin_system_config_media_gallery_renditions" type="Magento\MediaGalleryRenditions\Plugin\UpdateRenditionsOnConfigChange"/> + </type> + <type name="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface"> + <plugin name="delete_renditions_on_assets_delete" type="Magento\MediaGalleryRenditions\Plugin\RemoveRenditions"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml new file mode 100644 index 0000000000000..e3bb939158fec --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml @@ -0,0 +1,18 @@ +<?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:module:Magento_MediaContentApi:etc/media_content.xsd"> + <search> + <patterns> + <pattern name="media_gallery_renditions">/{{media url=(?:"|&quot;)(?:.renditions)?(.*?)(?:"|&quot;)}}/</pattern> + <pattern name="media_gallery">/{{media url="?((?!.*.renditions).*?)"?}}/</pattern> + <pattern name="wysiwyg">/src=".*\/media\/(?:.renditions\/)*(.*?)"/</pattern> + <pattern name="catalog_image">/^\/?media\/(?:.renditions\/)?(.*)/</pattern> + <pattern name="catalog_image_with_pub">/^\/pub\/?media\/(?:.renditions\/)?(.*)/</pattern> + </patterns> + </search> +</config> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml new file mode 100644 index 0000000000000..93bc9f1c214e6 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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_MediaGalleryRenditions"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml new file mode 100644 index 0000000000000..0c584ac12f898 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?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/consumer.xsd"> + <consumer name="media.gallery.renditions.update" queue="media.gallery.renditions.update" + connection="db" handler="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions::execute"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9618329895230 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<!-- +/** + * 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/publisher.xsd"> + <publisher topic="media.gallery.renditions.update"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml new file mode 100644 index 0000000000000..260e9f5f7f371 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml @@ -0,0 +1,14 @@ +<?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/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="MediaGalleryRenditions" topic="media.gallery.renditions.update" + destinationType="queue" destination="media.gallery.renditions.update"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php new file mode 100644 index 0000000000000..275c06f752a63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditions', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php new file mode 100644 index 0000000000000..6684fcc47b6c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +interface GenerateRenditionsInterface +{ + /** + * Generate image renditions + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php new file mode 100644 index 0000000000000..3f398dd37529b --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +interface GetRenditionPathInterface +{ + /** + * Get Renditions image path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + public function execute(string $path): string; +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md new file mode 100644 index 0000000000000..42478c0c9b520 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditionsApi module + +The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json new file mode 100644 index 0000000000000..6e3c559f001c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions-api", + "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.", + "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\\MediaGalleryRenditionsApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml new file mode 100644 index 0000000000000..f3a3f87b61105 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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_MediaGalleryRenditionsApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php new file mode 100644 index 0000000000000..bf057f2d2adbf --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditionsApi', + __DIR__ +); diff --git a/composer.json b/composer.json index 1af86e438882c..57fbfaaa35c2b 100644 --- a/composer.json +++ b/composer.json @@ -222,6 +222,8 @@ "magento/module-media-gallery-cms-ui": "*", "magento/module-media-gallery-catalog-integration": "*", "magento/module-media-gallery-catalog": "*", + "magento/module-media-gallery-renditions": "*", + "magento/module-media-gallery-renditions-api": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", "magento/module-msrp": "*", diff --git a/composer.lock b/composer.lock index 551167152be4d..8a5d82536cee4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aadcf8a265dd7ecbb86dd3dd4e49bc28", + "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", "packages": [ { "name": "colinmollenhour/cache-backend-file", From c889371002a38db883eb416dcd785e02b055c379 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Thu, 3 Sep 2020 22:19:46 +0800 Subject: [PATCH 60/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - updated PR with requested changes and fixed failed integration test --- ...talogTest.php => SynchronizeIdentitiesTest.php} | 13 ++++--------- ...esCmsTest.php => SynchronizeIdentitiesTest.php} | 14 ++++---------- 2 files changed, 8 insertions(+), 19 deletions(-) rename app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/{SynchronizeIdentitiesCatalogTest.php => SynchronizeIdentitiesTest.php} (93%) rename app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/{SynchronizeIdentitiesCmsTest.php => SynchronizeIdentitiesTest.php} (91%) diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php similarity index 93% rename from app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php rename to app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php index 4a1ba1445ffe5..5be72e2b4bf60 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCatalogTest.php +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -13,14 +13,13 @@ use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** * Test for catalog SynchronizeIdentities. */ -class SynchronizeIdentitiesCatalogTest extends TestCase +class SynchronizeIdentitiesTest extends TestCase { private const ENTITY_TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; @@ -46,17 +45,11 @@ class SynchronizeIdentitiesCatalogTest extends TestCase */ private $synchronizeIdentities; - /** - * @var SynchronizeInterface - */ - private $synchronize; - protected function setUp(): void { $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); - $this->synchronize = Bootstrap::getObjectManager()->get(SynchronizeInterface::class); $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); } @@ -70,6 +63,8 @@ protected function setUp(): void */ public function testExecute(array $mediaContentIdentities): void { + $assetId = 2020; + $contentIdentities = []; foreach ($mediaContentIdentities as $mediaContentIdentity) { $contentIdentities[] = $this->contentIdentityFactory->create( @@ -82,9 +77,9 @@ public function testExecute(array $mediaContentIdentities): void } $this->assertNotEmpty($contentIdentities); + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); $this->synchronizeIdentities->execute($contentIdentities); - $assetId = 2020; $entityIds = []; foreach ($contentIdentities as $contentIdentity) { $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php similarity index 91% rename from app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php rename to app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php index bdd8bfd1105d3..bde43f5477b8b 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesCmsTest.php +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -13,14 +13,13 @@ use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** * Test for CMS SynchronizeIdentities. */ -class SynchronizeIdentitiesCmsTest extends TestCase +class SynchronizeIdentitiesTest extends TestCase { private const ENTITY_TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; @@ -46,17 +45,11 @@ class SynchronizeIdentitiesCmsTest extends TestCase */ private $synchronizeIdentities; - /** - * @var SynchronizeInterface - */ - private $synchronize; - protected function setUp(): void { $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); - $this->synchronize = Bootstrap::getObjectManager()->get(SynchronizeInterface::class); $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); } @@ -70,6 +63,8 @@ protected function setUp(): void */ public function testExecute(array $mediaContentIdentities): void { + $assetId = 2020; + $contentIdentities = []; foreach ($mediaContentIdentities as $mediaContentIdentity) { $contentIdentities[] = $this->contentIdentityFactory->create( @@ -82,12 +77,11 @@ public function testExecute(array $mediaContentIdentities): void } $this->assertNotEmpty($contentIdentities); + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); $this->synchronizeIdentities->execute($contentIdentities); - $assetId = 2020; $entityIds = []; foreach ($contentIdentities as $contentIdentity) { - $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); $entityIds[] = $contentIdentity->getEntityId(); } From 739e6dc64cff5b7c90f219c103a7469f3dbcf071 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 3 Sep 2020 15:28:27 +0100 Subject: [PATCH 61/96] magento/magento2#29715: Moved ACL to MediaGalleryUiApi --- app/code/Magento/MediaGalleryApi/etc/acl.xml | 26 ------------------ .../Controller/Adminhtml/Category/Index.php | 2 +- .../media_gallery_category_listing.xml | 2 +- .../Controller/Adminhtml/Asset/Search.php | 2 +- .../Adminhtml/Directories/Create.php | 2 +- .../Adminhtml/Directories/Delete.php | 2 +- .../Adminhtml/Directories/GetTree.php | 2 +- .../Controller/Adminhtml/Image/Delete.php | 2 +- .../Controller/Adminhtml/Image/Details.php | 2 +- .../Adminhtml/Image/SaveDetails.php | 2 +- .../Controller/Adminhtml/Image/Upload.php | 2 +- .../Controller/Adminhtml/Index/Index.php | 2 +- .../Controller/Adminhtml/Media/Index.php | 2 +- .../Ui/Component/Control/CreateFolder.php | 2 +- .../Ui/Component/Control/DeleteAssets.php | 2 +- .../Ui/Component/Control/DeleteFolder.php | 2 +- .../Ui/Component/Control/InsertAsstes.php | 2 +- .../Ui/Component/Control/UploadAssets.php | 2 +- .../Ui/Component/DirectoryTree.php | 2 +- .../Ui/Component/Listing/Columns/Url.php | 4 +-- .../Listing/Massactions/Massaction.php | 2 +- .../MediaGalleryUi/etc/adminhtml/menu.xml | 4 +-- .../layout/media_gallery_index_index.xml | 2 +- .../ui_component/media_gallery_listing.xml | 2 +- .../standalone_media_gallery_listing.xml | 2 +- .../Magento/MediaGalleryUiApi/composer.json | 3 ++- .../Magento/MediaGalleryUiApi/etc/acl.xml | 27 +++++++++++++++++++ 27 files changed, 55 insertions(+), 53 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryApi/etc/acl.xml create mode 100644 app/code/Magento/MediaGalleryUiApi/etc/acl.xml diff --git a/app/code/Magento/MediaGalleryApi/etc/acl.xml b/app/code/Magento/MediaGalleryApi/etc/acl.xml deleted file mode 100644 index 50f7114d1f3be..0000000000000 --- a/app/code/Magento/MediaGalleryApi/etc/acl.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?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:Acl/etc/acl.xsd"> - <acl> - <resources> - <resource id="Magento_Backend::admin"> - <resource id="Magento_Backend::content"> - <resource id="Magento_Backend::content_elements"> - <resource id="Magento_MediaGalleryApi::media_gallery" title="Media Gallery" translate="title"> - <resource id="Magento_MediaGalleryApi::upload_assets" title="Upload Assets" translate="title" sortOrder="80"/> - <resource id="Magento_MediaGalleryApi::delete_assets" title="Delete Assets" translate="title" sortOrder="70"/> - <resource id="Magento_MediaGalleryApi::insert_assets" title="Insert Assets into the content" translate="title" sortOrder="60"/> - <resource id="Magento_MediaGalleryApi::create_folder" title="Create Folder" translate="title" sortOrder="50"/> - <resource id="Magento_MediaGalleryApi::delete_folder" title="Delete Folder" translate="title" sortOrder="40"/> - </resource> - </resource> - </resource> - </resource> - </resources> - </acl> -</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php index d2fb90f3bccff..a541e9999b784 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php @@ -18,7 +18,7 @@ */ class Index extends Action implements HttpGetActionInterface { - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; /** * Get the media gallery layout diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml index e289c19fe6219..e12d90b95303b 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -27,7 +27,7 @@ </storageConfig> <updateUrl path="mui/index/render"/> </settings> - <aclResource>Magento_MediaGalleryApi::media_gallery</aclResource> + <aclResource>Magento_Cms::media_gallery</aclResource> <dataProvider class="Magento\MediaGalleryCatalogUi\Model\Listing\DataProvider" name="media_gallery_category_listing_data_source"> <settings> <requestFieldName>entity_id</requestFieldName> diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php index 6aaa7d44cb508..9b6c08edbc86d 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -34,7 +34,7 @@ class Search extends Action implements HttpGetActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; /** * @var SearchAssetsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php index f65f9d622aeba..76c00927b33e0 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::create_folder'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::create_folder'; /** * @var CreateDirectoriesByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php index bda891f93e907..3dc43e5276860 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::delete_folder'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_folder'; /** * @var DeleteAssetsByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php index 0872685444193..d4885cae055dd 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -25,7 +25,7 @@ class GetTree extends Action implements HttpGetActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; /** * @var LoggerInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php index 0e91d85ed6a3a..2f7766c590033 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::delete_assets'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_assets'; /** * @var DeleteImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php index 3929ad8268f4e..d959a070148ed 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php @@ -29,7 +29,7 @@ class Details extends Action implements HttpGetActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; /** * @var GetDetailsByAssetId diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php index c4d2d3f07bed2..87a2e7345c407 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -32,7 +32,7 @@ class SaveDetails extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::edit_assets'; /** * @var UpdateAsset diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php index 902a253a78461..4492595bbe6ee 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::upload_assets'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::upload_assets'; /** * @var UploadImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php index 4e0544a2870b9..e97d93d86bb0d 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php @@ -18,7 +18,7 @@ */ class Index extends Action implements HttpGetActionInterface { - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; /** * @var LayoutFactory diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php index 6b297a618e517..8c5b3d4d3a9ac 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -21,7 +21,7 @@ */ class Index extends Action implements HttpGetActionInterface { - public const ADMIN_RESOURCE = 'Magento_MediaGalleryApi::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; /** * @var Config diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php index 1d71fa9892275..039a1006c79e5 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -13,7 +13,7 @@ */ class CreateFolder implements ButtonProviderInterface { - private const ACL_CREATE_FOLDER = 'Magento_MediaGalleryApi::create_folder'; + private const ACL_CREATE_FOLDER = 'Magento_MediaGalleryUiApi::create_folder'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php index a2078513c0730..10604d65f768f 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -13,7 +13,7 @@ */ class DeleteAssets implements ButtonProviderInterface { - private const ACL_DELETE_ASSETS= 'Magento_MediaGalleryApi::delete_assets'; + private const ACL_DELETE_ASSETS= 'Magento_MediaGalleryUiApi::delete_assets'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php index 3acafb2ae9c27..cb803c1c663e0 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -13,7 +13,7 @@ */ class DeleteFolder implements ButtonProviderInterface { - private const ACL_DELETE_FOLDER = 'Magento_MediaGalleryApi::delete_folder'; + private const ACL_DELETE_FOLDER = 'Magento_MediaGalleryUiApi::delete_folder'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php index ee04e7e8a5837..6854b79ba2c36 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php @@ -13,7 +13,7 @@ */ class InsertAsstes implements ButtonProviderInterface { - private const ACL_INSERT_ASSETS = 'Magento_MediaGalleryApi::insert_assets'; + private const ACL_INSERT_ASSETS = 'Magento_MediaGalleryUiApi::insert_assets'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php index 440695a367305..32bbdba88a599 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -13,7 +13,7 @@ */ class UploadAssets implements ButtonProviderInterface { - private const ACL_UPLOAD_ASSETS= 'Magento_MediaGalleryApi::upload_assets'; + private const ACL_UPLOAD_ASSETS= 'Magento_MediaGalleryUiApi::upload_assets'; /** * @var AuthorizationInterface diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index d42af8ef609ab..0ad5ad43f6157 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -18,7 +18,7 @@ class DirectoryTree extends Container { private const ACL_IMAGE_ACTIONS = [ - 'delete_folder' => 'Magento_MediaGalleryApi::delete_folder' + 'delete_folder' => 'Magento_MediaGalleryUiApi::delete_folder' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php index ffbd726cd5f47..05c82cf1b972c 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -23,8 +23,8 @@ class Url extends Column { private const ACL_IMAGE_ACTIONS = [ - 'insert_assets' => 'Magento_MediaGalleryApi::insert_assets', - 'delete_assets' => 'Magento_MediaGalleryApi::delete_assets' + 'insert_assets' => 'Magento_MediaGalleryUiApi::insert_assets', + 'delete_assets' => 'Magento_MediaGalleryUiApi::delete_assets' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php index e17048462c684..7d7b67125df96 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php @@ -17,7 +17,7 @@ class Massaction extends Container { private const ACL_IMAGE_ACTIONS = [ - 'delete_assets' => 'Magento_MediaGalleryApi::delete_assets' + 'delete_assets' => 'Magento_MediaGalleryUiApi::delete_assets' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml index afa73373bd407..92839aa75ac8b 100644 --- a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> - <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_MediaGalleryApi::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> - <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_MediaGalleryApi::media_gallery"/> + <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_Cms::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> + <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_Cms::media_gallery"/> </menu> </config> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml index fa03c477ec9a2..f41c0f91b2249 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -11,7 +11,7 @@ <block name="media.gallery.container" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::container.phtml" - aclResource="Magento_MediaGalleryApi::media_gallery"> + aclResource="Magento_Cms::media_gallery"> <container name="gallery.actions" htmlTag="div" htmlClass="page-main-actions"> <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> </container> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index 51c1dc21b016e..b7307f9a74fae 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -40,7 +40,7 @@ </storageConfig> <updateUrl path="mui/index/render"/> </settings> - <aclResource>Magento_MediaGalleryApi::media_gallery</aclResource> + <aclResource>Magento_Cms::media_gallery</aclResource> <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> <settings> <requestFieldName>id</requestFieldName> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index c1086ad891495..a53a46c61f75d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -33,7 +33,7 @@ </storageConfig> <updateUrl path="mui/index/render"/> </settings> - <aclResource>Magento_MediaGalleryApi::media_gallery</aclResource> + <aclResource>Magento_Cms::media_gallery</aclResource> <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> <settings> <requestFieldName>id</requestFieldName> diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json index f8d5ef11058c1..b3b3cc5092cab 100644 --- a/app/code/Magento/MediaGalleryUiApi/composer.json +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -3,7 +3,8 @@ "description": "Magento module responsible for the media gallery UI implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-cms": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUiApi/etc/acl.xml b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml new file mode 100644 index 0000000000000..c496c57d51322 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml @@ -0,0 +1,27 @@ +<?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:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Backend::content"> + <resource id="Magento_Backend::content_elements"> + <resource id="Magento_Cms::media_gallery" title="Media Gallery" translate="title"> + <resource id="Magento_MediaGalleryUiApi::insert_assets" title="Insert assets into the content" translate="title" sortOrder="40"/> + <resource id="Magento_MediaGalleryUiApi::upload_assets" title="Upload assets" translate="title" sortOrder="50"/> + <resource id="Magento_MediaGalleryUiApi::edit_assets" title="Edit asset details" translate="title" sortOrder="60"/> + <resource id="Magento_MediaGalleryUiApi::delete_assets" title="Delete assets" translate="title" sortOrder="70"/> + <resource id="Magento_MediaGalleryUiApi::create_folder" title="Create folder" translate="title" sortOrder="80"/> + <resource id="Magento_MediaGalleryUiApi::delete_folder" title="Delete folder" translate="title" sortOrder="90"/> + </resource> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> From 68bc010fc93a332082cfee97b700fa689161dd84 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 3 Sep 2020 17:54:34 +0100 Subject: [PATCH 62/96] magento/magento2#29715: Enforced ACL for context menu and view details --- .../Block/Adminhtml/ImageDetails.php | 98 +++++++++++++++++++ .../Ui/Component/Listing/Columns/Url.php | 6 +- .../layout/media_gallery_index_index.xml | 2 +- .../layout/media_gallery_media_index.xml | 2 +- .../adminhtml/templates/image_details.phtml | 31 +----- .../templates/image_details_standalone.phtml | 25 +---- .../adminhtml/web/js/grid/columns/image.js | 4 +- .../web/js/grid/columns/image/actions.js | 6 +- 8 files changed, 116 insertions(+), 58 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php new file mode 100644 index 0000000000000..94a2b3cf1047c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Image details block + * + * @api + */ +class ImageDetails extends Template +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Json + */ + private $json; + + /** + * @param AuthorizationInterface $authorization + * @param Template\Context $context + * @param array $data + * @param JsonHelper|null $jsonHelper + * @param DirectoryHelper|null $directoryHelper + */ + public function __construct( + AuthorizationInterface $authorization, + Json $json, + Template\Context $context, + array $data = [], + ?JsonHelper $jsonHelper = null, + ?DirectoryHelper $directoryHelper = null + ) { + $this->authorization = $authorization; + $this->json = $json; + parent::__construct($context, $data, $jsonHelper, $directoryHelper); + } + + /** + * Retrieve actions json + * + * @return string + */ + public function getActionsJson(): string + { + $actions = [ + [ + 'title' => __('Cancel'), + 'handler' => 'closeModal', + 'name' => 'cancel', + 'classes' => 'action-default scalable cancel action-quaternary' + ] + ]; + + if ($this->authorization->isAllowed('MediaGalleryUiApi::edit_assets')) { + $actions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('MediaGalleryUiApi::delete_assets')) { + $actions[] = [ + 'title' => __('Delete Image'), + 'handler' => 'deleteImageAction', + 'name' => 'delete', + 'classes' => 'action-default scalable delete action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('MediaGalleryUiApi::insert_assets')) { + $actions[] = [ + 'title' => __('Add Image'), + 'handler' => 'addImage', + 'name' => 'add-image', + 'classes' => 'scalable action-primary add-image-action' + ]; + } + + return $this->json->serialize($actions); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php index 05c82cf1b972c..0d48a0d0ff0e1 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -23,8 +23,10 @@ class Url extends Column { private const ACL_IMAGE_ACTIONS = [ - 'insert_assets' => 'Magento_MediaGalleryUiApi::insert_assets', - 'delete_assets' => 'Magento_MediaGalleryUiApi::delete_assets' + 'image-details' => 'Magento_Cms::media_gallery', + 'insert' => 'Magento_MediaGalleryUiApi::insert_assets', + 'delete' => 'Magento_MediaGalleryUiApi::delete_assets', + 'edit' => 'Magento_MediaGalleryUiApi::edit_assets' ]; /** diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml index f41c0f91b2249..a5eb247bd344f 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -16,7 +16,7 @@ <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> </container> <uiComponent name="media_gallery_listing"/> - <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails" template="Magento_MediaGalleryUi::image_details.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml index 7750f22b39ce7..7697519c40f4d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -10,7 +10,7 @@ <body> <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> <uiComponent name="standalone_media_gallery_listing"/> - <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml index 67733b2c6855d..a547f33adbbdb 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -use Magento\Backend\Block\Template; +use Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails; use Magento\Framework\Escaper; // phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength -/** @var Template $block */ +/** @var ImageDetails $block */ /** @var Escaper $escaper */ ?> @@ -73,32 +73,7 @@ use Magento\Framework\Escaper; "modalWindowSelector": ".media-gallery-image-details", "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", - "actionsList": [ - { - "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", - "handler": "editImageAction", - "name": "edit", - "classes": "action-default scalable edit action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", - "handler": "closeModal", - "name": "cancel", - "classes": "action-default scalable cancel action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", - "handler": "deleteImageAction", - "name": "delete", - "classes": "action-default scalable delete action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", - "handler": "addImage", - "name": "add-image", - "classes": "scalable action-primary add-image-action" - } - ] + "actionsList": <?= $block->getActionsJson() ?> } } } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml index f0b653cb8cd14..b4c80bf6d4196 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -use Magento\Backend\Block\Template; - // phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength -/** @var Template $block */ +/** @var \Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails $block */ /** @var \Magento\Framework\Escaper $escaper */ ?> @@ -71,26 +69,7 @@ use Magento\Backend\Block\Template; "modalWindowSelector": ".media-gallery-image-details", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", - "actionsList": [ - { - "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", - "handler": "editImageAction", - "name": "edit", - "classes": "action-default scalable edit action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", - "handler": "closeModal", - "name": "cancel", - "classes": "action-default scalable cancel action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", - "handler": "deleteImageAction", - "name": "delete", - "classes": "action-default scalable delete action-quaternary" - } - ] + "actionsList": <?= $block->getActionsJson() ?> } } } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js index ca731e693bd2d..c2a16170d58b8 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -228,11 +228,11 @@ define([ return; } - if (this.allowedActions.includes('insert_assets')) { + if (this.allowedActions.includes('insert')) { $(this.addSelectedBtnSelector).removeClass('no-display'); } - if (this.allowedActions.includes('delete_assets')) { + if (this.allowedActions.includes('delete')) { $(this.deleteSelectedBtnSelector).removeClass('no-display'); } }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js index ec959a06e1cce..10b94632d6745 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -55,7 +55,11 @@ define([ this._super(); this.initEvents(); - if (!this.allowedActions.includes('delete_assets')) { + this.actionsList = this.actionsList.filter(function(item) { + return this.allowedActions.includes(item.name); + }.bind(this)); + + if (!this.allowedActions.includes('delete')) { $.async('.media-gallery-delete-assets', function () { $('.media-gallery-delete-assets').unbind('click').addClass('action-disabled'); }); From 0ad0630ee1c426be5468fa223ecd012f06d339b1 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 3 Sep 2020 18:03:04 +0100 Subject: [PATCH 63/96] magento/magento2#29715: Corrected ACL resource names --- .../Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php index 94a2b3cf1047c..9f67ee5578e29 100644 --- a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php @@ -66,7 +66,7 @@ public function getActionsJson(): string ] ]; - if ($this->authorization->isAllowed('MediaGalleryUiApi::edit_assets')) { + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { $actions[] = [ 'title' => __('Edit Details'), 'handler' => 'editImageAction', @@ -75,7 +75,7 @@ public function getActionsJson(): string ]; } - if ($this->authorization->isAllowed('MediaGalleryUiApi::delete_assets')) { + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { $actions[] = [ 'title' => __('Delete Image'), 'handler' => 'deleteImageAction', @@ -84,7 +84,7 @@ public function getActionsJson(): string ]; } - if ($this->authorization->isAllowed('MediaGalleryUiApi::insert_assets')) { + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::insert_assets')) { $actions[] = [ 'title' => __('Add Image'), 'handler' => 'addImage', From 9ccfdc9ca4eb2f20ad9ac1b40f999f3f302c9951 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 3 Sep 2020 18:29:22 +0100 Subject: [PATCH 64/96] magento/magento2#29715: Added data upgrade script --- .../Patch/Data/AddMediaGalleryPermissions.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php new file mode 100644 index 0000000000000..60ffb2147bab6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; + +/** + * Patch is mechanism, that allows to do atomic upgrade data changes + */ +class AddMediaGalleryPermissions implements + DataPatchInterface, + PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface $moduleDataSetup + */ + private $moduleDataSetup; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup) + { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * Add child resources permissions for user roles with Magento_Cms::media_gallery permission + */ + public function apply(): void + { + $tableName = $this->moduleDataSetup->getTable('authorization_rule'); + $connection = $this->moduleDataSetup->getConnection(); + + if (!$tableName) { + return; + } + + $select = $connection->select() + ->from($tableName, ['role_id']) + ->where('resource_id = "Magento_Cms::media_gallery"'); + + $connection->insertMultiple($tableName, $this->getInsertData($connection->fetchCol($select))); + } + + /** + * Retrieve data to insert to authorization_rule table based on role ids + * + * @param array $roleIds + * @return array + */ + private function getInsertData(array $roleIds): array + { + $newResources = [ + 'Magento_MediaGalleryUiApi::insert_assets', + 'Magento_MediaGalleryUiApi::upload_assets', + 'Magento_MediaGalleryUiApi::edit_assets', + 'Magento_MediaGalleryUiApi::delete_assets', + 'Magento_MediaGalleryUiApi::create_folder', + 'Magento_MediaGalleryUiApi::delete_folder' + ]; + + $data = []; + + foreach ($roleIds as $roleId) { + foreach ($newResources as $resourceId) { + $data[] = [ + 'role_id' => $roleId, + 'resource_id' => $resourceId, + 'permission' => 'allow' + ]; + } + } + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.4.2'; + } +} From 6bc5a8a1c029b7d3802e5f3a4411815a8e3d16e5 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Fri, 4 Sep 2020 02:39:13 +0800 Subject: [PATCH 65/96] magento/adobe-stock-integration#1724: Support batches processing for synchronization queue messages - fixed failed integration test --- .../SynchronizeIdentitiesTest.php | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php index bde43f5477b8b..825542baaff8c 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -7,8 +7,13 @@ namespace Magento\MediaContentSynchronizationCms\Test\Integration\Model\Synchronizer; +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\IntegrationException; -use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; @@ -54,16 +59,29 @@ protected function setUp(): void } /** - * @dataProvider filesProvider * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php - * @param ContentIdentityInterface[] $mediaContentIdentities * @throws IntegrationException + * @throws LocalizedException */ - public function testExecute(array $mediaContentIdentities): void + public function testExecute(): void { $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $mediaContentIdentities = [ + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ], + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ]; $contentIdentities = []; foreach ($mediaContentIdentities as $mediaContentIdentity) { @@ -82,6 +100,7 @@ public function testExecute(array $mediaContentIdentities): void $entityIds = []; foreach ($contentIdentities as $contentIdentity) { + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); $entityIds[] = $contentIdentity->getEntityId(); } @@ -94,27 +113,46 @@ public function testExecute(array $mediaContentIdentities): void } /** - * Data provider + * Get fixture block * - * @return array + * @param string $identifier + * @return BlockInterface + * @throws LocalizedException */ - public function filesProvider(): array + private function getBlock(string $identifier): BlockInterface { - return [ - [ - [ - [ - 'entityType' => 'cms_page', - 'field' => 'content', - 'entityId' => 5 - ], - [ - 'entityType' => 'cms_block', - 'field' => 'content', - 'entityId' => 1 - ] - ] - ] - ]; + $objectManager = Bootstrap::getObjectManager(); + + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = $objectManager->get(BlockRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier) + ->create(); + + return current($blockRepository->getList($searchCriteria)->getItems()); + } + + /** + * Get fixture page + * + * @param string $identifier + * @return PageInterface + * @throws LocalizedException + */ + private function getPage(string $identifier): PageInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var PageRepositoryInterface $repository */ + $repository = $objectManager->get(PageRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier) + ->create(); + + return current($repository->getList($searchCriteria)->getItems()); } } From 90c9dac7173cc7755daf94eef37673473c97fed7 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 12:29:25 +0100 Subject: [PATCH 66/96] magento/magento2#29715: Corrected actions order --- .../Block/Adminhtml/ImageDetails.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php index 9f67ee5578e29..e3f9c5bafca22 100644 --- a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php @@ -66,15 +66,6 @@ public function getActionsJson(): string ] ]; - if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { - $actions[] = [ - 'title' => __('Edit Details'), - 'handler' => 'editImageAction', - 'name' => 'edit', - 'classes' => 'action-default scalable edit action-quaternary' - ]; - } - if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { $actions[] = [ 'title' => __('Delete Image'), @@ -84,6 +75,15 @@ public function getActionsJson(): string ]; } + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { + $actions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::insert_assets')) { $actions[] = [ 'title' => __('Add Image'), From 7beb815d9ec7060de67eef855b5709f8597ae5a4 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 12:49:13 +0100 Subject: [PATCH 67/96] magento/magento2#29889: Code review and static tests fixes --- app/code/Magento/MediaGalleryRenditions/Model/Config.php | 1 + .../MediaGalleryRenditions/Model/GenerateRenditions.php | 6 ++++++ .../Model/Queue/FetchRenditionPathsBatches.php | 2 +- .../Model/Queue/UpdateRenditions.php | 2 +- .../MediaGalleryRenditions/Plugin/RemoveRenditions.php | 8 ++++---- .../Plugin/UpdateRenditionsOnConfigChange.php | 2 +- app/code/Magento/MediaGalleryRenditions/composer.json | 2 ++ .../Magento/MediaGalleryRenditions/etc/media_content.xml | 2 +- .../Api/GenerateRenditionsInterface.php | 3 +++ .../Api/GetRenditionPathInterface.php | 3 +++ 10 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Config.php b/app/code/Magento/MediaGalleryRenditions/Model/Config.php index a40fbb41bd831..d1a48904d1f13 100644 --- a/app/code/Magento/MediaGalleryRenditions/Model/Config.php +++ b/app/code/Magento/MediaGalleryRenditions/Model/Config.php @@ -34,6 +34,7 @@ class Config /** * @param ScopeConfigInterface $scopeConfig + * @param ResourceConnection $resourceConnection */ public function __construct( ScopeConfigInterface $scopeConfig, diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php index d1bfcb82a6a84..6bc54fdf9aca4 100644 --- a/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php +++ b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php @@ -64,6 +64,8 @@ class GenerateRenditions implements GenerateRenditionsInterface * @param GetRenditionPathInterface $getRenditionPath * @param Filesystem $filesystem * @param File $driver + * @param IsPathExcludedInterface $isPathExcluded + * @param LoggerInterface $log */ public function __construct( AdapterFactory $imageFactory, @@ -121,6 +123,8 @@ public function execute(array $paths): void */ private function generateRendition(string $path): void { + $this->validateAsset($path); + $renditionPath = $this->getRenditionPath->execute($path); $this->createDirectory($renditionPath); @@ -137,6 +141,8 @@ private function generateRendition(string $path): void } /** + * Ensure valid media asset path is provided for renditions generation + * * @param string $path * @throws FileSystemException * @throws LocalizedException diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php index 9cec7edbbd39a..7263010a8f587 100644 --- a/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php @@ -44,7 +44,7 @@ class FetchRenditionPathsBatches /** * @param LoggerInterface $log * @param Filesystem $filesystem - * @param GetFilesIterator $assetsIterator + * @param GetFilesIterator $getFilesIterator * @param int $batchSize * @param array $fileExtensions */ diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php index 1fcc28d235f09..45cea58d05018 100644 --- a/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php @@ -69,7 +69,7 @@ public function execute(array $paths): void /** * Update renditions and log exceptions * - * @param string[] $paths + * @param string[] $renditionPaths */ private function updateRenditions(array $renditionPaths): void { diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php index dd75800e4384d..69f8e5b955cba 100644 --- a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php @@ -52,15 +52,15 @@ public function __construct( * Remove renditions when assets are removed * * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths - * @param \Closure $proceed + * @param null $result * @param array $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundExecute( + public function afterExecute( DeleteAssetsByPathsInterface $deleteAssetsByPaths, - \Closure $proceed, + $result, array $paths ): void { - $proceed($paths); $this->removeRenditions($paths); } diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php index 9feb63377add9..9cf969c16782f 100644 --- a/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php @@ -54,7 +54,7 @@ public function afterSave(Value $config, Value $result): Value * @param Value $value * @return bool */ - private function isRenditionsValue(Value $value) + private function isRenditionsValue(Value $value): bool { return $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH || $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH; diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json index 948f6ce3b14f3..669a62865e5be 100644 --- a/app/code/Magento/MediaGalleryRenditions/composer.json +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -5,7 +5,9 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-media-gallery-renditions-api": "*", + "magento/module-media-gallery-api": "*", "magento/module-media-content-api": "*", + "magento/framework-message-queue": "*", "magento/module-cms": "*" }, "type": "magento2-module", diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml index e3bb939158fec..a1fbe5cba558e 100644 --- a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml +++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml @@ -15,4 +15,4 @@ <pattern name="catalog_image_with_pub">/^\/pub\/?media\/(?:.renditions\/)?(.*)/</pattern> </patterns> </search> -</config> \ No newline at end of file +</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php index 6684fcc47b6c1..b3ad5543c17fa 100644 --- a/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php @@ -9,6 +9,9 @@ use Magento\Framework\Exception\LocalizedException; +/** + * Generate optimized version of media assets based on configuration for insertion to content + */ interface GenerateRenditionsInterface { /** diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php index 3f398dd37529b..b00c3615d9a29 100644 --- a/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php @@ -9,6 +9,9 @@ use Magento\Framework\Exception\LocalizedException; +/** + * Based on media assset path provides path to an optimized image version for insertion to the content + */ interface GetRenditionPathInterface { /** From 74325ef39fd1c1e4e957747dc2d29ca3e73f4cf8 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 4 Sep 2020 15:46:53 +0300 Subject: [PATCH 68/96] minor refactor --- app/code/Magento/Widget/Model/Widget.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index ec948063234da..72f7dd77577af 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -310,10 +310,14 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) { $widget = $this->getConfigAsObject($type); + $params = array_filter($params, function ($value) { + return $value !== null; + }); + $directiveParams = ''; foreach ($params as $name => $value) { // Retrieve default option value if pre-configured - $directiveParams .= $value === null ? '' : $this->getDirectiveParam($widget, $name, $value); + $directiveParams .= $this->getDirectiveParam($widget, $name, $value); } $directive = sprintf('{{widget type="%s"%s%s}}', $type, $directiveParams, $this->getWidgetPageVarName($params)); From 0ac2e5d691f5bccbdb2fba896bb1c22ff6a0fa80 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 4 Sep 2020 15:47:29 +0300 Subject: [PATCH 69/96] mftf coverage --- ...alogProductsListWidgetTitleActionGroup.xml | 22 ++++++++ ...StorefrontAssertWidgetTitleActionGroup.xml | 26 ++++++++++ .../InsertWidgetSection.xml | 1 + ...eFrontWidgetTitleWithReservedCharsTest.xml | 52 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml create mode 100644 app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml new file mode 100644 index 0000000000000..e146506d51a24 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.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="AdminFillCatalogProductsListWidgetTitleActionGroup"> + <annotations> + <description>Fill catalog products list title field.</description> + </annotations> + + <arguments> + <argument name="title" type="string" defaultValue=""/> + </arguments> + <waitForElementVisible selector="{{InsertWidgetSection.title}}" stepKey="waitForField"/> + <fillField selector="{{InsertWidgetSection.title}}" userInput="{{title}}" stepKey="fillTitleField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml new file mode 100644 index 0000000000000..4505680424471 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertWidgetTitleActionGroup"> + <annotations> + <description>Assert widget title on storefront.</description> + </annotations> + <arguments> + <argument name="title" type="string"/> + </arguments> + + <grabTextFrom selector="{{StorefrontWidgetsSection.widgetProductsGrid}} {{StorefrontWidgetsSection.widgetTitle}}" + stepKey="grabWidgetTitle"/> + <assertEquals stepKey="assertWidgetTitle"> + <actualResult type="string">$grabWidgetTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml index 9b40971611d6f..3d8d5ecc1cda9 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml @@ -19,5 +19,6 @@ <element name="checkElementStorefrontByPrice" type="button" selector="//*[@class='product-items widget-product-grid']//*[contains(text(),'${{arg4}}.00')]" parameterized="true"/> <element name="checkElementStorefrontByName" type="button" selector="//*[@class='product-items widget-product-grid']//*[@class='product-item'][{{productPosition}}]//a[contains(text(), '{{productName}}')]" parameterized="true"/> <element name="categoryTreeWrapper" type="text" selector=".rule-chooser .tree.x-tree"/> + <element name="title" type="text" selector="input[name='parameters[title]']"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml new file mode 100644 index 0000000000000..4cfdc8197ad84 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.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="StoreFrontWidgetTitleWithReservedCharsTest"> + <annotations> + <features value="Cms"/> + <stories value="Create a CMS Page via the Admin when widget title contains reserved chairs"/> + <title value="Create CMS Page via the Admin when widget title contains reserved chairs"/> + <description value="See CMS Page title on store front page if titled widget with reserved chairs added"/> + <severity value="MAJOR"/> + <group value="Cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProductWithoutCategory"/> + <createData entity="_defaultCmsPage" stepKey="createCmsPage"/> + </before> + <after> + <deleteData createDataKey="createSimpleProductWithoutCategory" stepKey="deleteProduct"/> + <deleteData createDataKey="createCmsPage" stepKey="deleteCmsPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Navigate to Page in Admin--> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$createCmsPage$"/> + </actionGroup> + <!--Insert widget--> + <actionGroup ref="AdminInsertWidgetToCmsPageContentActionGroup" stepKey="insertWidgetToCmsPageContent"> + <argument name="widgetType" value="Catalog Products List"/> + </actionGroup> + <!--Fill widget title and save--> + <actionGroup ref="AdminFillCatalogProductsListWidgetTitleActionGroup" stepKey="fillWidgetTitle"> + <argument name="title" value="Tittle }}"/> + </actionGroup> + <actionGroup ref="AdminClickInsertWidgetActionGroup" stepKey="clickInsertWidgetButton"/> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="saveOpenedPage"/> + <!--Verify data on frontend--> + <actionGroup ref="StorefrontGoToCMSPageActionGroup" stepKey="navigateToPageOnStorefront"> + <argument name="identifier" value="$createCmsPage.identifier$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertWidgetTitleActionGroup" stepKey="verifyPageDataOnFrontend"> + <argument name="title" value="Tittle }}"/> + </actionGroup> + </test> +</tests> From f7cd6d77439722059b0a7f8ef7c295f67d53c0bd Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 13:58:06 +0100 Subject: [PATCH 70/96] magento/magento2#29889: Removed redundant dependency --- app/code/Magento/MediaGalleryRenditions/composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json index 669a62865e5be..873e0b4a8c60b 100644 --- a/app/code/Magento/MediaGalleryRenditions/composer.json +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -6,10 +6,12 @@ "magento/framework": "*", "magento/module-media-gallery-renditions-api": "*", "magento/module-media-gallery-api": "*", - "magento/module-media-content-api": "*", "magento/framework-message-queue": "*", "magento/module-cms": "*" }, + "suggest": { + "magento/module-media-content-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", From 748eb4f13da880c022203fca256a1cfebd96b778 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 14:08:49 +0100 Subject: [PATCH 71/96] magento/magento2#29715: Corrected dependencies --- app/code/Magento/MediaGalleryCatalogUi/composer.json | 1 + app/code/Magento/MediaGalleryUi/composer.json | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json index 418e113072ae3..985d581beff25 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/composer.json +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -4,6 +4,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/module-cms": "*", "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-store": "*", diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index 6008bb1579c49..f4701306eb369 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -12,8 +12,7 @@ "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-synchronization-api": "*", "magento/module-media-content-api": "*", - "magento/module-cms": "*", - "magento/module-media-gallery": "*" + "magento/module-cms": "*" }, "type": "magento2-module", "license": [ From b3d52559d3529b4ad1f8d28dfb200d6c579a94e7 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 14:39:53 +0100 Subject: [PATCH 72/96] magento/magento2#29715: Corrected standalone actions and upgrade script --- .../Adminhtml/ImageDetailsStandalone.php | 89 +++++++++++++++++++ .../Patch/Data/AddMediaGalleryPermissions.php | 8 +- .../layout/media_gallery_media_index.xml | 2 +- 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php new file mode 100644 index 0000000000000..44637770271a6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Directory\Helper\Data as DirectoryHelperData; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Json\Helper\Data as JsonHelperData; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Image details block + * + * @api + */ +class ImageDetailsStandalone extends Template +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Json + */ + private $json; + + /** + * @param AuthorizationInterface $authorization + * @param Template\Context $context + * @param array $data + * @param JsonHelperData|null $jsonHelper + * @param DirectoryHelperData|null $directoryHelper + */ + public function __construct( + AuthorizationInterface $authorization, + Json $json, + Template\Context $context, + array $data = [], + ?JsonHelperData $jsonHelper = null, + ?DirectoryHelperData $directoryHelper = null + ) { + $this->authorization = $authorization; + $this->json = $json; + parent::__construct($context, $data, $jsonHelper, $directoryHelper); + } + + /** + * Retrieve actions json + * + * @return string + */ + public function getActionsJson(): string + { + $actions = [ + [ + 'title' => __('Cancel'), + 'handler' => 'closeModal', + 'name' => 'cancel', + 'classes' => 'action-default scalable cancel action-quaternary' + ] + ]; + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { + $actions[] = [ + 'title' => __('Delete Image'), + 'handler' => 'deleteImageAction', + 'name' => 'delete', + 'classes' => 'action-default scalable delete action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { + $actions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + + return $this->json->serialize($actions); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php index 60ffb2147bab6..f3b554e207037 100644 --- a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -78,12 +78,14 @@ private function getInsertData(array $roleIds): array ]; } } + + return $data; } /** * @inheritdoc */ - public function getAliases() + public function getAliases(): array { return []; } @@ -91,7 +93,7 @@ public function getAliases() /** * @inheritdoc */ - public static function getDependencies() + public static function getDependencies(): array { return []; } @@ -99,7 +101,7 @@ public static function getDependencies() /** * @inheritdoc */ - public static function getVersion() + public static function getVersion(): string { return '2.4.2'; } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml index 7697519c40f4d..b4f377627c850 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -10,7 +10,7 @@ <body> <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> <uiComponent name="standalone_media_gallery_listing"/> - <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetailsStandalone" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> From 0a6156f038ff20ba31ada254baf0855b4f045f9d Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 14:51:19 +0100 Subject: [PATCH 73/96] magento/magento2#29715: Fixed upgrade --- .../Setup/Patch/Data/AddMediaGalleryPermissions.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php index f3b554e207037..e72017e20a7f6 100644 --- a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -47,7 +47,11 @@ public function apply(): void ->from($tableName, ['role_id']) ->where('resource_id = "Magento_Cms::media_gallery"'); - $connection->insertMultiple($tableName, $this->getInsertData($connection->fetchCol($select))); + $insertData = $this->getInsertData($connection->fetchCol($select)); + + if (!empty($insertData)) { + $connection->insertMultiple($tableName, $insertData); + } } /** From 70b0d06f94d24903e04c7d4690f737ed1b73d0b1 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@gmail.com> Date: Fri, 4 Sep 2020 16:59:28 +0300 Subject: [PATCH 74/96] adobe-stock-integration#1792: fix category image is not removed from tmp folder after category save --- .../Plugin/SaveBaseCategoryImageInformation.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php index d439b53c120cb..b683ec8fe9d91 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -81,17 +81,18 @@ public function __construct( * * @param ImageUploader $subject * @param string $imagePath + * @param string $initialImageName * @return string * @throws LocalizedException */ - public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string + public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath, string $initialImageName): string { if (!$this->config->isEnabled()) { return $imagePath; } $absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath; - $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1); + $tmpPath = $subject->getBaseTmpPath() . '/' . $initialImageName; $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); if (!empty($tmpAssets)) { From 635971ffaec3d80af20da051e685f4106d677f64 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 19:13:19 +0100 Subject: [PATCH 75/96] Fixed static test --- .../MediaGalleryRenditions/Plugin/RemoveRenditions.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php index 69f8e5b955cba..f05c34e703abe 100644 --- a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php @@ -7,6 +7,7 @@ namespace Magento\MediaGalleryRenditions\Plugin; +use Magento\Catalog\Helper\Data as CatalogHelper; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; @@ -18,6 +19,11 @@ */ class RemoveRenditions { + /** + * @var CatalogHelper + */ + private $catalogHelper; + /** * @var GetRenditionPathInterface */ @@ -39,10 +45,12 @@ class RemoveRenditions * @param LoggerInterface $log */ public function __construct( + CatalogHelper $catalogHelper, GetRenditionPathInterface $getRenditionPath, Filesystem $filesystem, LoggerInterface $log ) { + $this->catalogHelper = $catalogHelper; $this->getRenditionPath = $getRenditionPath; $this->filesystem = $filesystem; $this->log = $log; @@ -52,7 +60,7 @@ public function __construct( * Remove renditions when assets are removed * * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths - * @param null $result + * @param void $result * @param array $paths * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ From 253bafbc254692717a8e090b67da129a72b4f061 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 4 Sep 2020 19:34:57 +0100 Subject: [PATCH 76/96] magento/magento2#29715: Fixed tests --- .../MediaGalleryUi/Block/Adminhtml/ImageDetails.php | 5 +++-- .../Block/Adminhtml/ImageDetailsStandalone.php | 13 +++++++------ .../Test/AdminMediaGalleryCreateFolderAclTest.xml | 4 ++-- .../Test/AdminMediaGalleryDeleteAssetsAclTest.xml | 4 ++-- .../Test/AdminMediaGalleryDeleteFolderAclTest.xml | 6 +++--- .../Test/AdminMediaGalleryUploadAssetsAclTest.xml | 4 ++-- app/code/Magento/MediaGalleryUi/composer.json | 4 +++- .../view/adminhtml/templates/image_details.phtml | 2 +- .../templates/image_details_standalone.phtml | 2 +- .../adminhtml/web/js/grid/columns/image/actions.js | 2 +- 10 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php index e3f9c5bafca22..d797acedda6ec 100644 --- a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php @@ -31,16 +31,17 @@ class ImageDetails extends Template private $json; /** - * @param AuthorizationInterface $authorization * @param Template\Context $context + * @param AuthorizationInterface $authorization + * @param Json $json * @param array $data * @param JsonHelper|null $jsonHelper * @param DirectoryHelper|null $directoryHelper */ public function __construct( + Template\Context $context, AuthorizationInterface $authorization, Json $json, - Template\Context $context, array $data = [], ?JsonHelper $jsonHelper = null, ?DirectoryHelper $directoryHelper = null diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php index 44637770271a6..7e73b1682f79a 100644 --- a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php @@ -31,16 +31,17 @@ class ImageDetailsStandalone extends Template private $json; /** - * @param AuthorizationInterface $authorization * @param Template\Context $context + * @param AuthorizationInterface $authorization + * @param Json $json * @param array $data * @param JsonHelperData|null $jsonHelper * @param DirectoryHelperData|null $directoryHelper */ public function __construct( + Template\Context $context, AuthorizationInterface $authorization, Json $json, - Template\Context $context, array $data = [], ?JsonHelperData $jsonHelper = null, ?DirectoryHelperData $directoryHelper = null @@ -57,7 +58,7 @@ public function __construct( */ public function getActionsJson(): string { - $actions = [ + $standaloneActions = [ [ 'title' => __('Cancel'), 'handler' => 'closeModal', @@ -67,7 +68,7 @@ public function getActionsJson(): string ]; if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { - $actions[] = [ + $standaloneActions[] = [ 'title' => __('Delete Image'), 'handler' => 'deleteImageAction', 'name' => 'delete', @@ -76,7 +77,7 @@ public function getActionsJson(): string } if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { - $actions[] = [ + $standaloneActions[] = [ 'title' => __('Edit Details'), 'handler' => 'editImageAction', 'name' => 'edit', @@ -84,6 +85,6 @@ public function getActionsJson(): string ]; } - return $this->json->serialize($actions); + return $this->json->serialize($standaloneActions); } } diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml index 8e1e4bbfbd48a..9738ddedc3cc3 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml @@ -44,7 +44,7 @@ <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Create Folder"/> + <argument name="restrictedRole" value="Create folder"/> </actionGroup> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> <argument name="User" value="adminRole"/> @@ -58,7 +58,7 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> - + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> <argument name="username" value="{{admin2.username}}"/> <argument name="password" value="{{admin2.password}}"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml index bdfb5c475b3d8..1d51caf0fc400 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml @@ -44,7 +44,7 @@ <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Assets"/> + <argument name="restrictedRole" value="Delete assets"/> </actionGroup> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> @@ -59,7 +59,7 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> - + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> <argument name="username" value="{{admin2.username}}"/> <argument name="password" value="{{admin2.password}}"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml index f261e1bd4601c..121ad25c93f0d 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml @@ -44,9 +44,9 @@ <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Delete Folder"/> + <argument name="restrictedRole" value="Delete folder"/> </actionGroup> - + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> <argument name="User" value="adminRole"/> <argument name="restrictedRole" value="Pages"/> @@ -59,7 +59,7 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> - + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> <argument name="username" value="{{admin2.username}}"/> <argument name="password" value="{{admin2.password}}"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml index 4dae0b1ec5510..c8f8655d11edb 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml @@ -44,7 +44,7 @@ <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> <argument name="User" value="adminRole"/> - <argument name="restrictedRole" value="Upload Assets"/> + <argument name="restrictedRole" value="Upload assets"/> </actionGroup> <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> @@ -59,7 +59,7 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> - + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> <argument name="username" value="{{admin2.username}}"/> <argument name="password" value="{{admin2.password}}"/> diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index f4701306eb369..204e0b37c3bf8 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -12,7 +12,9 @@ "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-synchronization-api": "*", "magento/module-media-content-api": "*", - "magento/module-cms": "*" + "magento/module-cms": "*", + "magento/module-directory": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml index a547f33adbbdb..5df5c1a6c4cbd 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -73,7 +73,7 @@ use Magento\Framework\Escaper; "modalWindowSelector": ".media-gallery-image-details", "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", - "actionsList": <?= $block->getActionsJson() ?> + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> } } } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml index b4c80bf6d4196..fdae0a549606c 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -69,7 +69,7 @@ "modalWindowSelector": ".media-gallery-image-details", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", - "actionsList": <?= $block->getActionsJson() ?> + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> } } } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js index 10b94632d6745..76e051072285a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -55,7 +55,7 @@ define([ this._super(); this.initEvents(); - this.actionsList = this.actionsList.filter(function(item) { + this.actionsList = this.actionsList.filter(function (item) { return this.allowedActions.includes(item.name); }.bind(this)); From ee1d2d2bc57de68d4d57114e28001f546d5cc78c Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sun, 6 Sep 2020 11:34:40 +0100 Subject: [PATCH 77/96] magento/magento2#29543: Removed general operations from partial synchronizer --- .../Model/SynchronizeIdentities.php | 54 +++---------------- ...Pool.php => SynchronizeIdentitiesPool.php} | 2 +- .../etc/di.xml | 2 +- .../MediaContentSynchronizationCms/etc/di.xml | 2 +- 4 files changed, 11 insertions(+), 49 deletions(-) rename app/code/Magento/MediaContentSynchronizationApi/Model/{SynchronizerIdentitiesPool.php => SynchronizeIdentitiesPool.php} (97%) diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php index 0cd9b9bca6784..1bf57c6b2ec42 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -11,7 +11,7 @@ use Magento\Framework\FlagManager; use Magento\Framework\Stdlib\DateTime\DateTimeFactory; use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -use Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool; +use Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool; use Psr\Log\LoggerInterface; /** @@ -19,52 +19,26 @@ */ class SynchronizeIdentities implements SynchronizeIdentitiesInterface { - private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; - - /** - * @var DateTimeFactory - */ - private $dateFactory; - - /** - * @var FlagManager - */ - private $flagManager; - /** * @var LoggerInterface */ private $log; /** - * @var SynchronizerIdentitiesPool - */ - private $synchronizerIdentitiesPool; - - /** - * @var RemoveObsoleteContentAsset + * @var SynchronizeIdentitiesPool */ - private $removeObsoleteContent; + private $synchronizeIdentitiesPool; /** - * @param RemoveObsoleteContentAsset $removeObsoleteContent - * @param DateTimeFactory $dateFactory - * @param FlagManager $flagManager * @param LoggerInterface $log - * @param SynchronizerIdentitiesPool $synchronizerIdentitiesPool + * @param SynchronizeIdentitiesPool $synchronizeIdentitiesPool */ public function __construct( - RemoveObsoleteContentAsset $removeObsoleteContent, - DateTimeFactory $dateFactory, - FlagManager $flagManager, LoggerInterface $log, - SynchronizerIdentitiesPool $synchronizerIdentitiesPool + SynchronizeIdentitiesPool $synchronizeIdentitiesPool ) { - $this->removeObsoleteContent = $removeObsoleteContent; - $this->dateFactory = $dateFactory; - $this->flagManager = $flagManager; $this->log = $log; - $this->synchronizerIdentitiesPool = $synchronizerIdentitiesPool; + $this->synchronizeIdentitiesPool = $synchronizeIdentitiesPool; } /** @@ -74,9 +48,9 @@ public function execute(array $mediaContentIdentities): void { $failed = []; - foreach ($this->synchronizerIdentitiesPool->get() as $name => $synchronizers) { + foreach ($this->synchronizeIdentitiesPool->get() as $name => $synchronizer) { try { - $synchronizers->execute($mediaContentIdentities); + $synchronizer->execute($mediaContentIdentities); } catch (\Exception $exception) { $this->log->critical($exception); $failed[] = $name; @@ -93,17 +67,5 @@ public function execute(array $mediaContentIdentities): void ) ); } - - $this->setLastExecutionTime(); - $this->removeObsoleteContent->execute(); - } - - /** - * Set last synchronizer execution time - */ - private function setLastExecutionTime(): void - { - $currentTime = $this->dateFactory->create()->gmtDate(); - $this->flagManager->saveFlag(self::LAST_EXECUTION_TIME_CODE, $currentTime); } } diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php similarity index 97% rename from app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php rename to app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php index 2f4a503625e91..1ea957d5cd6e7 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerIdentitiesPool.php +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php @@ -9,7 +9,7 @@ use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; -class SynchronizerIdentitiesPool +class SynchronizeIdentitiesPool { /** * Content with assets synchronizers diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml index d26ed06a42586..070f25f501712 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -38,7 +38,7 @@ </argument> </arguments> </type> - <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool"> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool"> <arguments> <argument name="synchronizers" xsi:type="array"> <item name="media_content_catalog" diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml index 94e9c686aafbb..d6e7604c71d97 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -14,7 +14,7 @@ </argument> </arguments> </type> - <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerIdentitiesPool"> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool"> <arguments> <argument name="synchronizers" xsi:type="array"> <item name="media_content_cms" From 2d1d097b39bdd94ef6e929c34fc721113a3505ea Mon Sep 17 00:00:00 2001 From: "Zeno F. Pensky" <zeno.pensky@sitewards.com> Date: Mon, 7 Sep 2020 12:55:02 +0200 Subject: [PATCH 78/96] Fix #29879 Breadcrump Undefined class constant 'XML_PATH_CATEGORY_URL_SUFFIX' When creating a plugin, that is calling the public functions for the product breadcrump they ending up throwing an error message, because this constants has been marked as private recently. This is fixing it with referencing to the current instance and all is working again. This fix is analog to another bug of the same type: https://github.com/magento/magento2/issues/28981 https://github.com/magento/magento2/pull/28797 This should resolve: https://github.com/magento/magento2/issues/29879 --- app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php index d3c8c406ee34d..2aa30fb18fdf4 100644 --- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php +++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php @@ -71,7 +71,7 @@ public function __construct( public function getCategoryUrlSuffix() { return $this->scopeConfig->getValue( - static::XML_PATH_CATEGORY_URL_SUFFIX, + self::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE ); } @@ -84,7 +84,7 @@ public function getCategoryUrlSuffix() public function isCategoryUsedInProductUrl(): bool { return $this->scopeConfig->isSetFlag( - static::XML_PATH_PRODUCT_USE_CATEGORIES, + self::XML_PATH_PRODUCT_USE_CATEGORIES, ScopeInterface::SCOPE_STORE ); } From edd340130b92015e3ecd6e03e93ca75111610afe Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Mon, 7 Sep 2020 13:55:55 +0300 Subject: [PATCH 79/96] improve array filter --- app/code/Magento/Widget/Model/Widget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 72f7dd77577af..c8b13ef028592 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -311,7 +311,7 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) $widget = $this->getConfigAsObject($type); $params = array_filter($params, function ($value) { - return $value !== null; + return $value !== null && $value !== ''; }); $directiveParams = ''; From 9fd54363ced22b9d627f0768cf1820d125468e80 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Mon, 7 Sep 2020 14:38:11 +0300 Subject: [PATCH 80/96] refactor; add strict type --- .../Unit/Block/Product/View/Type/ConfigurableTest.php | 6 +++--- app/code/Magento/Tax/Pricing/Render/Adjustment.php | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 9abcfc6efaee3..08279c55c5b30 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -256,10 +256,10 @@ public function cacheKeyProvider(): array */ public function testGetCacheKeyInfo( array $expected, - string $priceCurrency = null, - string $customerGroupId = null + ?string $priceCurrency = null, + ?int $customerGroupId = null ): void { - $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + $storeMock = $this->getMockBuilder(StoreInterface::class) ->setMethods(['getCurrentCurrency']) ->getMockForAbstractClass(); $storeMock->expects($this->any()) diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php index ec451eb012878..0e5c619790a97 100644 --- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php +++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php @@ -181,11 +181,10 @@ public function displayPriceExcludingTax() * * @return string */ - public function getDataPriceType() + public function getDataPriceType(): string { - if ($this->getData('price_type') && $this->getData('price_type') !== 'finalPrice') { - return 'base' . ucfirst($this->getData('price_type')); - } - return 'basePrice'; + return $this->amountRender->getPriceType() === 'finalPrice' + ? 'basePrice' + : 'base' . ucfirst($this->amountRender->getPriceType()); } } From 879a74ea12c529f9a93e64ec1c95bf37164f5d80 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@gmail.com> Date: Mon, 7 Sep 2020 16:19:41 +0300 Subject: [PATCH 81/96] adobe-stock-integration#1792: cover by mftf test --- ...SameImageDeleteFromTemporaryFolderTest.xml | 48 +++++++++++++++++++ ...diaGalleryCatalogUiCategoryGridSection.xml | 1 + ...sertMediaGalleryEmptyFolderActionGroup.xml | 17 +++++++ ...lleryExpandCatalogTmpFolderActionGroup.xml | 21 ++++++++ ...lleryFolderSelectByFullPathActionGroup.xml | 16 +++++++ 5 files changed, 103 insertions(+) create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml new file mode 100644 index 0000000000000..c138ea81e7f7b --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml @@ -0,0 +1,48 @@ +<?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="AdminUploadSameImageDeleteFromTemporaryFolderTest"> + <annotations> + <features value="AdminUploadSameImageDeleteFromTemporaryFolderTest"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1792"/> + <title value="Image is deleted from tmp folder if is uploaded second time"/> + <description value="Image is deleted from tmp folder if is uploaded second time"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <!-- Upload test image to category twice --> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImageSecondTime"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryFormSecondTime"/> + + <!-- Open tmp/category folder --> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup" stepKey="expandTmpFolder"/> + <actionGroup ref="AdminMediaGalleryFolderSelectByFullPathActionGroup" stepKey="selectCategoryFolder"> + <argument name="name" value="catalog/tmp/category"/> + </actionGroup> + + <!-- Asset folder is empty --> + <actionGroup ref="AdminAssertMediaGalleryEmptyFolderActionGroup" stepKey="assertEmptyFolder"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index f65ec84bc2ec8..9baa4bd3c3268 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -14,5 +14,6 @@ <element name="image" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img[contains(@src, '{{file}}')]" parameterized="true"/> <element name="columnValue" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{columnName}}')]/preceding-sibling::th) +1 ]//div" parameterized="true"/> <element name="edit" type="button" selector="//tr[td//text()[contains(., '{{categoryName}}')]]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Action')]/preceding-sibling::th) +1 ]//*[text()='{{actionButton}}']" parameterized="true"/> + <element name="noDataMessage" type="text" selector="div.no-data-message-container"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml new file mode 100644 index 0000000000000..e6f8327b814a5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertMediaGalleryEmptyFolderActionGroup"> + <annotations> + <description>Requires select folder in directory tree. Assert that selected folder is empty.</description> + </annotations> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.noDataMessage}}" stepKey="assertNoDataMessageDisplayed" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml new file mode 100644 index 0000000000000..db9d1853df583 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml @@ -0,0 +1,21 @@ +<?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="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup"> + <annotations> + <description>Expand media gallery tmp folder tree</description> + </annotations> + <waitForLoadingMaskToDisappear stepKey="waitLoadingMask"/> + <conditionalClick selector="//li[@id='catalog']/ins" dependentSelector="//li[@id='catalog']/ul" visible="false" stepKey="expandCatalog"/> + <wait time="2" stepKey="waitCatalogExpanded"/> + <conditionalClick selector="//li[@id='catalog/tmp']/ins" dependentSelector="//li[@id='catalog/tmp']/ul" visible="false" stepKey="expandTmp"/> + <wait time="2" stepKey="waitTmpExpanded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml new file mode 100644 index 0000000000000..717278d5c8f26 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml @@ -0,0 +1,16 @@ +<?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="AdminMediaGalleryFolderSelectByFullPathActionGroup" + extends="AdminMediaGalleryFolderSelectActionGroup"> + <remove keyForRemoval="selectFolder"/> + <click selector="//li[@id='{{name}}']" stepKey="selectSubFolder" after="waitBeforeClickOnFolder"/> + </actionGroup> +</actionGroups> From 5b93a7692be050669f511d679e12c2ae75c7bc8e Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Mon, 7 Sep 2020 18:57:11 +0300 Subject: [PATCH 82/96] MC-35416: It is not possible to Manage Shopping Cart from Customer edit page after adding product to Wish List --- .../Model/ResourceModel/Item/Collection.php | 2 +- .../Model/ResourceModel/Item/CollectionTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 5d9b1911bc292..73b7b6c827ec1 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -398,7 +398,7 @@ protected function _renderFiltersBefore() $availableProductTypes = $this->salesConfig->getAvailableProductTypes(); $this->getSelect()->join( ['cat_prod' => $this->getTable('catalog_product_entity')], - $this->getConnection()->quoteInto('cat_prod.type_id IN (?)', $availableProductTypes), + $this->getConnection()->quoteInto('cat_prod.type_id IN (?) AND main_table.product_id = cat_prod.entity_id', $availableProductTypes), [] ); } diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php index 9a95ed4fd462d..e347ee88af902 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php @@ -62,6 +62,20 @@ public function testLoadedProductAttributes() $this->assertEquals('Short description', $productOnWishlist->getData('short_description')); } + /** + * Tests collection load. + * Tests collection load method when product salable filter flag is setted to true + * and few products are present. + * + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testGonnaCutYouDown() + { + $this->itemCollection->setSalableFilter(true); + $this->itemCollection->load(); + } + /** * @param array $attributes */ From 7296cdfd222561023571c1a6294115a0d5d512e2 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 7 Sep 2020 17:45:35 +0100 Subject: [PATCH 83/96] magento/magento2#29715: Removed redundant dependency --- app/code/Magento/MediaGalleryUiApi/composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json index b3b3cc5092cab..d577f50523f13 100644 --- a/app/code/Magento/MediaGalleryUiApi/composer.json +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -3,7 +3,9 @@ "description": "Magento module responsible for the media gallery UI implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", + "magento/framework": "*" + }, + "suggest": { "magento/module-cms": "*" }, "type": "magento2-module", From 15e4522a7a18f251105360064eb6ff7be5287537 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 7 Sep 2020 17:52:58 +0100 Subject: [PATCH 84/96] magento/magento2#29889: Fixed static tests --- .../MediaGalleryRenditions/Plugin/RemoveRenditions.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php index f05c34e703abe..f0ba8c3533722 100644 --- a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php @@ -7,7 +7,6 @@ namespace Magento\MediaGalleryRenditions\Plugin; -use Magento\Catalog\Helper\Data as CatalogHelper; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; @@ -19,11 +18,6 @@ */ class RemoveRenditions { - /** - * @var CatalogHelper - */ - private $catalogHelper; - /** * @var GetRenditionPathInterface */ @@ -45,12 +39,10 @@ class RemoveRenditions * @param LoggerInterface $log */ public function __construct( - CatalogHelper $catalogHelper, GetRenditionPathInterface $getRenditionPath, Filesystem $filesystem, LoggerInterface $log ) { - $this->catalogHelper = $catalogHelper; $this->getRenditionPath = $getRenditionPath; $this->filesystem = $filesystem; $this->log = $log; From 7f0a49594f61ddfe2c0fa57c0fb7b8e9fbdc3329 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Tue, 8 Sep 2020 09:41:32 +0300 Subject: [PATCH 85/96] adobe-stock-integration#1792: update after review --- ...inUploadSameImageDeleteFromTemporaryFolderTest.xml | 6 +++--- .../AdminMediaGalleryCatalogUiCategoryGridSection.xml | 1 - ...ml => AdminAssertMediaGalleryEmptyActionGroup.xml} | 4 ++-- ...nMediaGalleryFolderSelectByFullPathActionGroup.xml | 11 +++++++---- ...sertAdminEnhancedMediaGallerySortByActionGroup.xml | 6 +++--- ...onSection.xml => AdminMediaGalleryGridSection.xml} | 3 ++- 6 files changed, 17 insertions(+), 14 deletions(-) rename app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/{AdminAssertMediaGalleryEmptyFolderActionGroup.xml => AdminAssertMediaGalleryEmptyActionGroup.xml} (71%) rename app/code/Magento/MediaGalleryUi/Test/Mftf/Section/{AdminEnhancedMediaGalleryGridImagePositionSection.xml => AdminMediaGalleryGridSection.xml} (77%) diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml index c138ea81e7f7b..252027b27bf6d 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml @@ -39,10 +39,10 @@ <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> <actionGroup ref="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup" stepKey="expandTmpFolder"/> <actionGroup ref="AdminMediaGalleryFolderSelectByFullPathActionGroup" stepKey="selectCategoryFolder"> - <argument name="name" value="catalog/tmp/category"/> + <argument name="path" value="catalog/tmp/category"/> </actionGroup> - <!-- Asset folder is empty --> - <actionGroup ref="AdminAssertMediaGalleryEmptyFolderActionGroup" stepKey="assertEmptyFolder"/> + <!-- Assert folder is empty --> + <actionGroup ref="AdminAssertMediaGalleryEmptyActionGroup" stepKey="assertEmptyFolder"/> </test> </tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index 9baa4bd3c3268..f65ec84bc2ec8 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -14,6 +14,5 @@ <element name="image" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img[contains(@src, '{{file}}')]" parameterized="true"/> <element name="columnValue" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{columnName}}')]/preceding-sibling::th) +1 ]//div" parameterized="true"/> <element name="edit" type="button" selector="//tr[td//text()[contains(., '{{categoryName}}')]]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Action')]/preceding-sibling::th) +1 ]//*[text()='{{actionButton}}']" parameterized="true"/> - <element name="noDataMessage" type="text" selector="div.no-data-message-container"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml similarity index 71% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml index e6f8327b814a5..c212092b657fd 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyFolderActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml @@ -7,11 +7,11 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminAssertMediaGalleryEmptyFolderActionGroup"> + <actionGroup name="AdminAssertMediaGalleryEmptyActionGroup"> <annotations> <description>Requires select folder in directory tree. Assert that selected folder is empty.</description> </annotations> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.noDataMessage}}" stepKey="assertNoDataMessageDisplayed" /> + <seeElement selector="{{AdminMediaGalleryGridSection.noDataMessage}}" stepKey="assertNoDataMessageDisplayed" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml index 717278d5c8f26..49aa45426152c 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml @@ -8,9 +8,12 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminMediaGalleryFolderSelectByFullPathActionGroup" - extends="AdminMediaGalleryFolderSelectActionGroup"> - <remove keyForRemoval="selectFolder"/> - <click selector="//li[@id='{{name}}']" stepKey="selectSubFolder" after="waitBeforeClickOnFolder"/> + <actionGroup name="AdminMediaGalleryFolderSelectByFullPathActionGroup"> + <arguments> + <argument name="path" type="string"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//li[@id='{{path}}']" stepKey="selectSubFolder" after="waitBeforeClickOnFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml index 451ef81f0ff9f..53781a65e4898 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml @@ -18,11 +18,11 @@ <argument name="thirdImageFile" type="string"/> </arguments> - <grabAttributeFrom selector="{{AdminEnhancedMediaGalleryGridImagePositionSection.nthImageInGrid('0')}}" userInput="src" + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('0')}}" userInput="src" stepKey="getFirstImageSrcAfterSort"/> - <grabAttributeFrom selector="{{AdminEnhancedMediaGalleryGridImagePositionSection.nthImageInGrid('1')}}" userInput="src" + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('1')}}" userInput="src" stepKey="getSecondImageSrcAfterSort"/> - <grabAttributeFrom selector="{{AdminEnhancedMediaGalleryGridImagePositionSection.nthImageInGrid('2')}}" userInput="src" + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('2')}}" userInput="src" stepKey="getThirdImageSrcAfterSort"/> <assertStringContainsString stepKey="assertFirstImagePositionAfterSort"> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml similarity index 77% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml index 943f29d5fa851..f35a32b6d3a37 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml @@ -7,7 +7,8 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminEnhancedMediaGalleryGridImagePositionSection"> + <section name="AdminMediaGalleryGridSection"> + <element name="noDataMessage" type="text" selector="div.no-data-message-container"/> <element name="nthImageInGrid" type="text" selector="div[class='masonry-image-column'][data-repeat-index='{{row}}'] img" parameterized="true"/> </section> </sections> From ace8a6fd54d920b01533668f7f0b2942ec4b3def Mon Sep 17 00:00:00 2001 From: Vadim Malesh <51680850+engcom-Charlie@users.noreply.github.com> Date: Tue, 8 Sep 2020 09:48:44 +0300 Subject: [PATCH 86/96] add testCaseId --- .../Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml index 4cfdc8197ad84..bc379ec424fce 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml @@ -14,6 +14,7 @@ <title value="Create CMS Page via the Admin when widget title contains reserved chairs"/> <description value="See CMS Page title on store front page if titled widget with reserved chairs added"/> <severity value="MAJOR"/> + <testCaseId value="MC-37419"/> <group value="Cms"/> <group value="WYSIWYGDisabled"/> </annotations> From 61d15651ef53bf51ae1ec14fd4f68125280c65ee Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Tue, 8 Sep 2020 11:00:49 +0300 Subject: [PATCH 87/96] MC-35416: It is not possible to Manage Shopping Cart from Customer edit page after adding product to Wish List --- .../Wishlist/Model/ResourceModel/Item/Collection.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 73b7b6c827ec1..04e320ce4bc50 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -398,7 +398,11 @@ protected function _renderFiltersBefore() $availableProductTypes = $this->salesConfig->getAvailableProductTypes(); $this->getSelect()->join( ['cat_prod' => $this->getTable('catalog_product_entity')], - $this->getConnection()->quoteInto('cat_prod.type_id IN (?) AND main_table.product_id = cat_prod.entity_id', $availableProductTypes), + $this->getConnection() + ->quoteInto( + 'cat_prod.type_id IN (?) AND main_table.product_id = cat_prod.entity_id', + $availableProductTypes + ), [] ); } From 047228139f8fd8f10decf7f442e356913dd4f90b Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Tue, 8 Sep 2020 13:43:39 +0300 Subject: [PATCH 88/96] fix static --- .../creditmemo/items/renderer/default.phtml | 24 ++++++++++------- .../invoice/items/renderer/default.phtml | 16 ++++++----- .../order/items/renderer/default.phtml | 27 ++++++++++--------- .../shipment/items/renderer/default.phtml | 19 +++++++------ 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml index c7a1b4b79b91a..029bcb8abcc25 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml @@ -10,15 +10,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,7 +27,7 @@ </div> <?php endif; ?> </dd> - <?php else : ?> + <?php else: ?> <dd> <?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?> </dd> @@ -37,10 +37,10 @@ <?php endif; ?> <?php /* downloadable */ ?> - <?php if ($links = $block->getLinks()) : ?> + <?php if ($links = $block->getLinks()): ?> <dl class="item options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($links->getPurchasedItems() as $link) : ?> + <?php foreach ($links->getPurchasedItems() as $link): ?> <dd><?= $block->escapeHtml($link->getLinkTitle()) ?></dd> <?php endforeach; ?> </dl> @@ -48,12 +48,14 @@ <?php /* EOF downloadable */ ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) : ?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> @@ -61,7 +63,9 @@ <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> <?= $block->getItemRowTotalHtml() ?> </td> - <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"><?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?></td> + <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"> + <?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?> + </td> <td class="col total" data-th="<?= $block->escapeHtml(__('Row Total')) ?>"> <?= $block->getItemRowTotalAfterDiscountHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml index d05d6d01a5340..d9542d13aba6d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml @@ -10,15 +10,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,19 +27,21 @@ </div> <?php endif; ?> </dd> - <?php else : ?> + <?php else: ?> <dd><?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml index f0a0f46265a3e..9cae232ca6541 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml @@ -10,15 +10,15 @@ $_item = $block->getItem(); <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,43 +27,46 @@ $_item = $block->getItem(); </div> <?php endif; ?> </dd> - <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else: ?> + <?php $optionValue = isset($_option['print_value']) ? $_option['print_value'] : $_option['value'] ?> + <dd><?= $block->escapeHtml($optionValue) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addtInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addtInfoBlock) : ?> + <?php if ($addtInfoBlock): ?> <?= $addtInfoBlock->setItem($_item)->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <ul class="items-qty"> - <?php if ($block->getItem()->getQtyOrdered() > 0) : ?> + <?php if ($block->getItem()->getQtyOrdered() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Ordered')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyOrdered() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyShipped() > 0) : ?> + <?php if ($block->getItem()->getQtyShipped() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Shipped')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyShipped() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyCanceled() > 0) : ?> + <?php if ($block->getItem()->getQtyCanceled() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Canceled')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyCanceled() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyRefunded() > 0) : ?> + <?php if ($block->getItem()->getQtyRefunded() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Refunded')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyRefunded() ?></span> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml index 773100601d2b8..6c7567a8cd14b 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml @@ -9,15 +9,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -26,18 +26,21 @@ </div> <?php endif; ?> </dd> - <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else: ?> + <?php $optionValue = isset($_option['print_value']) ? $_option['print_value'] : $_option['value'] ?> + <dd><?= $block->escapeHtml($optionValue) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) : ?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"><?= (int) $_item->getQty() ?></td> </tr> From c0960da3827f6bd205d229ef21f780bda3efc1c0 Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Tue, 8 Sep 2020 18:14:42 +0300 Subject: [PATCH 89/96] MC-35416: It is not possible to Manage Shopping Cart from Customer edit page after adding product to Wish List --- .../Magento/Wishlist/Model/ResourceModel/Item/Collection.php | 2 +- .../Wishlist/Model/ResourceModel/Item/CollectionTest.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 04e320ce4bc50..7d30d958b5228 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -400,7 +400,7 @@ protected function _renderFiltersBefore() ['cat_prod' => $this->getTable('catalog_product_entity')], $this->getConnection() ->quoteInto( - 'cat_prod.type_id IN (?) AND main_table.product_id = cat_prod.entity_id', + "cat_prod.type_id IN (?) AND {$mainTableName}.product_id = cat_prod.entity_id", $availableProductTypes ), [] diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php index e347ee88af902..1cbdf6144d640 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php @@ -69,11 +69,14 @@ public function testLoadedProductAttributes() * * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * @magentoDbIsolation disabled */ - public function testGonnaCutYouDown() + public function testLoadWhenFewProductsPresent() { $this->itemCollection->setSalableFilter(true); + $this->itemCollection->addCustomerIdFilter(1); $this->itemCollection->load(); + $this->assertCount(1, $this->itemCollection->getItems()); } /** From 5584b4cf2258a18fb17ca81022dfb09206f8c852 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 8 Sep 2020 19:00:17 +0100 Subject: [PATCH 90/96] Added stories annotation for the test --- .../Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml index 252027b27bf6d..ec15693be81e4 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml @@ -14,6 +14,7 @@ <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1792"/> <title value="Image is deleted from tmp folder if is uploaded second time"/> <description value="Image is deleted from tmp folder if is uploaded second time"/> + <stories value="Image is deleted from tmp folder if is uploaded second time"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> </annotations> From 35f7747fd853a90b3c9316c13f6da9ce62dc41ec Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Wed, 9 Sep 2020 02:37:06 +0800 Subject: [PATCH 91/96] magento/adobe-stock-integration#1801: User is not scrolled up to the Login failed. Please check if the Secret Key... error, and can miss the message - modified scroll to top script --- .../view/adminhtml/web/js/grid/columns/image.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js index bf852d0ddae68..4ff8eb36114d0 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -13,6 +13,8 @@ define([ return Column.extend({ defaults: { bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image', + messageContentSelector: 'ul.messages', + mediaGalleryContainerSelector: '.media-gallery-container', deleteImageUrl: 'media_gallery/image/delete', addSelectedBtnSelector: '#add_selected', deleteSelectedBtnSelector: '#delete_selected', @@ -270,6 +272,7 @@ define([ */ addMessage: function (code, message) { this.messages().add(code, message); + this.scrollToMessageContent(); this.messages().scheduleCleanup(); }, @@ -284,6 +287,20 @@ define([ !this.massaction().massActionMode()) { this.deselectImage(); } + }, + + /** + * Scroll to the top of media gallery page + */ + scrollToMessageContent: function () { + var scrollTargetElement = $(this.messageContentSelector), + scrollTargetContainer = $(this.mediaGalleryContainerSelector); + + scrollTargetContainer.find(scrollTargetElement).get(0).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); } }); }); From cef2186708426f7683cbd38d308562b2aa80ff46 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 9 Sep 2020 08:26:11 +0300 Subject: [PATCH 92/96] refactor mftf --- ...inGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename app/code/Magento/Ui/Test/Mftf/Test/{AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml => AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml} (98%) diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml similarity index 98% rename from app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml rename to app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml index 09c87010b5743..c7236c33e7cc0 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFilters.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml @@ -7,12 +7,13 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminGridFilterRemoveErrorMessageBeforeApplyFilters"> + <test name="AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest"> <annotations> <stories value="Reset Error Messages"/> <title value="Remove Error Message Before Apply Filters"/> <description value="Test login to Admin UI and Remove Error Message Before Apply Filters"/> - <severity value="CRITICAL"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37450"/> <group value="ui"/> </annotations> From 699c98fa1a2db63badd7441697c1da4b0dabb370 Mon Sep 17 00:00:00 2001 From: Vadim Malesh <51680850+engcom-Charlie@users.noreply.github.com> Date: Wed, 9 Sep 2020 10:58:16 +0300 Subject: [PATCH 93/96] add testCaseId --- ...torefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml index 4e01f950cd087..0e2ae9bf5cc5f 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml @@ -12,7 +12,8 @@ <stories value="Bundle product details page"/> <title value="Customer should be able to see all the bundle items in invoice view"/> <description value="Customer should be able to see all the bundle items in invoice view"/> - <severity value="CRITICAL"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37515"/> <group value="Bundle"/> </annotations> <before> From 86cc39c38f0d6111de014cb0b3f41841f377d67a Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Wed, 9 Sep 2020 09:52:37 +0100 Subject: [PATCH 94/96] magento/magento2#29906: Added test case id for AdminUploadSameImageDeleteFromTemporaryFolderTest --- .../Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml index ec15693be81e4..8add2021f056b 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml @@ -15,6 +15,7 @@ <title value="Image is deleted from tmp folder if is uploaded second time"/> <description value="Image is deleted from tmp folder if is uploaded second time"/> <stories value="Image is deleted from tmp folder if is uploaded second time"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> </annotations> From 01b7906cd3e339c0467f6cc9af6d681c7627482b Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 9 Sep 2020 13:32:33 +0300 Subject: [PATCH 95/96] revert changing the obsolete method --- app/code/Magento/Widget/Model/Widget.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index c8b13ef028592..b05b70cfcbc71 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -143,7 +143,8 @@ public function getWidgetByClassType($type) */ public function getConfigAsXml($type) { - return $this->getWidgetByClassType($type); + // phpstan:ignore + return $this->getXmlElementByType($type); } /** From b9daa9ea6b7bd57fdbb1dcf741bc1b8be2d0d713 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 10 Sep 2020 20:46:35 +0100 Subject: [PATCH 96/96] Skipped MFTF tests related to isolated issue --- .../AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml | 3 +++ .../Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml | 3 +++ .../Test/AdminMediaGalleryDisabledContentFilterTest.xml | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml index 4230640e76303..5a375d9153a6d 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryAssertUsedInLinkPagesGridTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="AdminMediaGalleryUsedInBlocksFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> <title value="Used in pages link"/> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml index 038f1ae077b4a..e72e65cf8de90 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryCmsUiUsedInPagesFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="AdminMediaGalleryUsedInPagesFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> <title value="Used in pages filter"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml index 963a0b954e45b..5926b115afccf 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryDisabledContentFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> <title value="User filter asset by disabled content"/> @@ -58,8 +61,8 @@ <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> <argument name="imageName" value="{{ImageMetadata.title}}"/> </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - + </test> </tests>