From 7185e1602d4e2298cfc79d88212002c169d79d00 Mon Sep 17 00:00:00 2001 From: Simon Sprankel Date: Wed, 15 Jan 2020 14:56:49 +0100 Subject: [PATCH 001/195] Add event prefix and object --- .../Product/Option/Value/Collection.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php index 5ea71176429fc..58e6290a820cd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php @@ -14,6 +14,20 @@ */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { + /** + * Name prefix of events that are dispatched by model + * + * @var string + */ + protected $_eventPrefix = 'catalog_product_option_value_collection'; + + /** + * Name of event parameter + * + * @var string + */ + protected $_eventObject = 'product_option_value_collection'; + /** * Resource initialization * From e91a3f1d29ecb07dca42535e796023d0db0fe7b5 Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki Date: Fri, 21 Feb 2020 16:32:02 +0100 Subject: [PATCH 002/195] Passing arguments for queues as expected --- .../Config/QueueConfigItem/DataMapperTest.php | 60 +++++++++++++------ .../Config/QueueConfigItem/DataMapper.php | 45 +++++++++----- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index cc5c4ac84440d..5fdf1db436fcd 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -1,29 +1,35 @@ configData = $this->createMock(Data::class); $this->communicationConfig = $this->createMock(CommunicationConfig::class); @@ -40,7 +49,12 @@ protected function setUp() $this->model = new DataMapper($this->configData, $this->communicationConfig, $this->queueNameBuilder); } - public function testGetMappedData() + /** + * @return void + * + * @throws LocalizedException + */ + public function testGetMappedData(): void { $data = [ 'ex01' => [ @@ -96,7 +110,9 @@ public function testGetMappedData() ['topic02', ['name' => 'topic02', 'is_synchronous' => false]], ]; - $this->communicationConfig->expects($this->exactly(2))->method('getTopic')->willReturnMap($communicationMap); + $this->communicationConfig->expects($this->exactly(2)) + ->method('getTopic') + ->willReturnMap($communicationMap); $this->configData->expects($this->once())->method('get')->willReturn($data); $this->queueNameBuilder->expects($this->once()) ->method('getQueueName') @@ -110,23 +126,27 @@ public function testGetMappedData() 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'some.queue--amqp' => [ 'name' => 'some.queue', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], ]; $this->assertEquals($expectedResult, $actualResult); } /** + * @return void + * + * @throws LocalizedException + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testGetMappedDataForWildcard() + public function testGetMappedDataForWildcard(): void { $data = [ 'ex01' => [ @@ -200,7 +220,9 @@ public function testGetMappedDataForWildcard() ->method('getTopic') ->with('topic01') ->willReturn(['name' => 'topic01', 'is_synchronous' => true]); - $this->communicationConfig->expects($this->any())->method('getTopics')->willReturn($communicationData); + $this->communicationConfig->expects($this->any()) + ->method('getTopics') + ->willReturn($communicationData); $this->configData->expects($this->once())->method('get')->willReturn($data); $this->queueNameBuilder->expects($this->any()) ->method('getQueueName') @@ -215,49 +237,49 @@ public function testGetMappedDataForWildcard() 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'some.queue--amqp' => [ 'name' => 'some.queue', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic02--amqp' => [ 'name' => 'responseQueue.topic02', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic03--amqp' => [ 'name' => 'responseQueue.topic03', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic04.04.04--amqp' => [ 'name' => 'responseQueue.topic04.04.04', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic05.05--amqp' => [ 'name' => 'responseQueue.topic05.05', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic08.part2.some.test--amqp' => [ 'name' => 'responseQueue.topic08.part2.some.test', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ] ]; $this->assertEquals($expectedResult, $actualResult); diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index 7e8d35fb0940f..627dca68d14a4 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -1,14 +1,17 @@ mappedData) { $this->mappedData = []; @@ -68,12 +72,18 @@ public function getMappedData() $connection = $exchange['connection']; foreach ($exchange['bindings'] as $binding) { if ($binding['destinationType'] === 'queue') { - $queueItems = $this->createQueueItems($binding['destination'], $binding['topic'], $connection); - $this->mappedData = array_merge($this->mappedData, $queueItems); + $queueItems = $this->createQueueItems( + (string) $binding['destination'], + (string) $binding['topic'], + (array) $binding['arguments'], + (string) $connection + ); + $this->mappedData += $queueItems; } } } } + return $this->mappedData; } @@ -82,10 +92,12 @@ public function getMappedData() * * @param string $name * @param string $topic + * @param array $arguments * @param string $connection * @return array + * @throws LocalizedException */ - private function createQueueItems($name, $topic, $connection) + private function createQueueItems(string $name, string $topic, array $arguments, string $connection): array { $output = []; $synchronousTopics = []; @@ -103,7 +115,7 @@ private function createQueueItems($name, $topic, $connection) 'connection' => $connection, 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => $arguments, ]; } @@ -112,8 +124,9 @@ private function createQueueItems($name, $topic, $connection) 'connection' => $connection, 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => $arguments, ]; + return $output; } @@ -124,15 +137,14 @@ private function createQueueItems($name, $topic, $connection) * @return bool * @throws LocalizedException */ - private function isSynchronousTopic($topicName) + private function isSynchronousTopic(string $topicName): bool { try { $topic = $this->communicationConfig->getTopic($topicName); - $isSync = (bool)$topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; - } catch (LocalizedException $e) { + return (bool) $topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + } catch (LocalizedException $exception) { throw new LocalizedException(new Phrase('Error while checking if topic is synchronous')); } - return $isSync; } /** @@ -141,22 +153,24 @@ private function isSynchronousTopic($topicName) * @param string $wildcard * @return array */ - private function matchSynchronousTopics($wildcard) + private function matchSynchronousTopics(string $wildcard): array { $topicDefinitions = array_filter( $this->communicationConfig->getTopics(), function ($item) { - return (bool)$item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + return (bool) $item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; } ); $topics = []; $pattern = $this->buildWildcardPattern($wildcard); + foreach (array_keys($topicDefinitions) as $topicName) { if (preg_match($pattern, $topicName)) { $topics[$topicName] = $topicName; } } + return $topics; } @@ -166,11 +180,10 @@ function ($item) { * @param string $wildcardKey * @return string */ - private function buildWildcardPattern($wildcardKey) + private function buildWildcardPattern(string $wildcardKey): string { $pattern = '/^' . str_replace('.', '\.', $wildcardKey); - $pattern = str_replace('#', '.+', $pattern); - $pattern = str_replace('*', '[^\.]+', $pattern); + $pattern = str_replace(['#', '*'], ['.+', '[^\.]+'], $pattern); $pattern .= strpos($wildcardKey, '#') === strlen($wildcardKey) ? '/' : '$/'; return $pattern; } From 73106d48473c3f78f965df22212df68fbf4da301 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk Date: Thu, 9 Apr 2020 15:06:49 +0300 Subject: [PATCH 003/195] Focus search field when pressing '/' --- .../adminhtml/Magento/backend/web/js/theme.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 05d73ac20fcbd..996f6c05935f2 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -345,6 +345,25 @@ define('globalSearch', [ this.input.on('focus.activateGlobalSearchForm', function () { self.field.addClass(self.options.fieldActiveClass); }); + + $(document).keydown(function (e) { + var inputs = [ + 'input', + 'select', + 'textarea' + ]; + + if (e.which !== 191 || // forward slash - '/' + inputs.indexOf(e.target.tagName.toLowerCase()) !== -1 || + e.target.isContentEditable + ) { + return; + } + + e.preventDefault(); + + self.input.focus(); + }); } }); From 29621ba467e4c6e145558bba9b378e29b9192d56 Mon Sep 17 00:00:00 2001 From: Matei Purcaru Date: Fri, 24 Apr 2020 16:15:56 +0300 Subject: [PATCH 004/195] ISSUE-27954: Forgot password save user only one column --- .../ConfirmCustomerByToken.php | 16 +++++++--------- .../ForgotPasswordToken/GetCustomerByToken.php | 2 +- .../Customer/Model/ResourceModel/Customer.php | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php index 6aadc814a4b9b..e8e9ac9764c3b 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php @@ -7,7 +7,7 @@ namespace Magento\Customer\Model\ForgotPasswordToken; -use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; /** * Confirm customer by reset password token @@ -20,22 +20,22 @@ class ConfirmCustomerByToken private $getByToken; /** - * @var CustomerRepositoryInterface + * @var CustomerResource */ - private $customerRepository; + private $customerResource; /** * ConfirmByToken constructor. * * @param GetCustomerByToken $getByToken - * @param CustomerRepositoryInterface $customerRepository + * @param CustomerResource $customerResource */ public function __construct( GetCustomerByToken $getByToken, - CustomerRepositoryInterface $customerRepository + CustomerResource $customerResource ) { $this->getByToken = $getByToken; - $this->customerRepository = $customerRepository; + $this->customerResource = $customerResource; } /** @@ -50,9 +50,7 @@ public function execute(string $resetPasswordToken): void { $customer = $this->getByToken->execute($resetPasswordToken); if ($customer->getConfirmation()) { - $this->customerRepository->save( - $customer->setConfirmation(null) - ); + $this->customerResource->updateColumn($customer->getId(), 'confirmation', null); } } } diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php index 09af4e296bd92..7ea4031cb5512 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php @@ -54,7 +54,7 @@ public function __construct( * @throws NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException */ - public function execute(string $resetPasswordToken):CustomerInterface + public function execute(string $resetPasswordToken): CustomerInterface { $this->searchCriteriaBuilder->addFilter( 'rp_token', diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index 1477287f79f4b..e0a79822ebeb8 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -403,4 +403,20 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c } return $this; } + + /** + * @param int $customerId + * @param string $column + * @param string $value + */ + public function updateColumn($customerId, $column, $value) + { + $this->getConnection()->update( + $this->getTable('customer_entity'), + [$column => $value], + [$this->getEntityIdField() . ' = ?' => $customerId] + ); + + return $this; + } } From a100d245f0feeed948ffe322bbd2cd26b9909855 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 02:43:42 +0200 Subject: [PATCH 005/195] Introduce separate BlockByIdentifier class to get Layout Block based on CMS Block Identifier --- app/code/Magento/Cms/Block/Block.php | 2 + .../Magento/Cms/Block/BlockByIdentifier.php | 145 ++++++++++++++ .../Test/Unit/Block/BlockByIdentifierTest.php | 183 ++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 app/code/Magento/Cms/Block/BlockByIdentifier.php create mode 100644 app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index 86cf059525e1e..afc95d369f67d 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -10,6 +10,8 @@ /** * Cms block content block + * @deprecated This class introduces caching issues and should no longer be used + * @see \Magento\Cms\Block\BlockByIdentifier */ class Block extends AbstractBlock implements \Magento\Framework\DataObject\IdentityInterface { diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php new file mode 100644 index 0000000000000..11dbf03642289 --- /dev/null +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -0,0 +1,145 @@ +` definition + */ +class BlockByIdentifier extends AbstractBlock implements IdentityInterface +{ + const CACHE_KEY_PREFIX = 'CMS_BLOCK'; + + /** + * @var GetBlockByIdentifierInterface + */ + private $blockByIdentifier; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var FilterProvider + */ + private $filterProvider; + + /** + * @var BlockInterface + */ + private $cmsBlock; + + public function __construct( + GetBlockByIdentifierInterface $blockByIdentifier, + StoreManagerInterface $storeManager, + FilterProvider $filterProvider, + Context $context, + array $data = [] + ) { + parent::__construct($context, $data); + $this->blockByIdentifier = $blockByIdentifier; + $this->storeManager = $storeManager; + $this->filterProvider = $filterProvider; + } + + /** + * @inheritDoc + */ + protected function _toHtml(): string + { + try { + return $this->filterOutput( + $this->getCmsBlock()->getContent() + ); + } catch (NoSuchEntityException $e) { + return ''; + } + } + + /** + * Filters the Content + * + * @param string $content + * @return string + * @throws NoSuchEntityException + */ + private function filterOutput(string $content): string + { + return $this->filterProvider->getBlockFilter() + ->setStoreId($this->getCurrentStore()->getId()) + ->filter($content); + } + + /** + * Loads the CMS block by `identifier` provided as an argument + * + * @return BlockInterface + * @throws NoSuchEntityException + */ + private function getCmsBlock(): BlockInterface + { + if (!$this->getIdentifier()) { + throw new NoSuchEntityException( + __('Expected value of `identifier` was not provided') + ); + } + + if (null === $this->cmsBlock) { + $this->cmsBlock = $this->blockByIdentifier->execute( + (string)$this->getIdentifier(), + (int)$this->getCurrentStore()->getId() + ); + } + + return $this->cmsBlock; + } + + /** + * Returns the StoreInterface of currently opened Store scope + * + * @return StoreInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getCurrentStore(): StoreInterface + { + return $this->storeManager->getStore(); + } + + /** + * Returns array of Block Identifiers used to determine Cache Tags + * + * This implementation supports different CMS blocks caching having the same identifier, + * resolving the bug introduced in scope of \Magento\Cms\Block\Block + * + * @return string[] + */ + public function getIdentities(): array + { + try { + return [ + self::CACHE_KEY_PREFIX . '_' . $this->getCmsBlock()->getId(), + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStore()->getId() + ]; + } catch (NoSuchEntityException $e) { + // If CMS Block does not exist, it should not be cached + return []; + } + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php new file mode 100644 index 0000000000000..b3e582be61446 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -0,0 +1,183 @@ +storeMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); + + $this->getBlockByIdentifierMock = $this->createMock(GetBlockByIdentifierInterface::class); + + $this->filterProviderMock = $this->createMock(FilterProvider::class); + $this->filterProviderMock->method('getBlockFilter')->willReturn($this->getPassthroughFilterMock()); + } + + public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void + { + // Given + $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); + + // Expect + $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); + $this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities()); + } + + public function testBlockReturnsCmsContentsWhenIdentifierFound(): void + { + // Given + $cmsBlockMock = $this->getCmsBlockMock( + self::STUB_CMS_BLOCK_ID, + self::STUB_EXISTING_IDENTIFIER, + self::STUB_CONTENT + ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + $this->getBlockByIdentifierMock->method('execute') + ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) + ->willReturn($cmsBlockMock); + $block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER); + + // Expect + $this->assertSame(self::ASSERT_CONTENT_HTML, $block->toHtml()); + } + + public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void + { + // Given + $cmsBlockMock = $this->getCmsBlockMock( + self::STUB_CMS_BLOCK_ID, + self::STUB_EXISTING_IDENTIFIER, + self::STUB_CONTENT + ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + $this->getBlockByIdentifierMock->method('execute') + ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) + ->willReturn($cmsBlockMock); + $block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER); + + // When + $identities = $block->getIdentities(); + + // Then + $this->assertContains($this->getCacheKeyStubById(self::STUB_CMS_BLOCK_ID), $identities); + $this->assertContains( + $this->getCacheKeyStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), + $identities + ); + } + + /** + * Initializes the tested block with injecting the references required by parent classes. + * + * @param string|null $identifier + * @return BlockByIdentifier + */ + private function getTestedBlockUsingIdentifier(?string $identifier): BlockByIdentifier + { + $eventManagerMock = $this->createMock(ManagerInterface::class); + $scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $scopeConfigMock->method('getValue')->willReturn(self::STUB_MODULE_OUTPUT_DISABLED); + + $contextMock = $this->createMock(Context::class); + $contextMock->method('getEventManager')->willReturn($eventManagerMock); + $contextMock->method('getScopeConfig')->willReturn($scopeConfigMock); + + return new BlockByIdentifier( + $this->getBlockByIdentifierMock, + $this->storeManagerMock, + $this->filterProviderMock, + $contextMock, + ['identifier' => $identifier] + ); + } + + /** + * Mocks the CMS Block object for further play + * + * @param int $entityId + * @param string $identifier + * @param string $content + * @return MockObject|BlockInterface + */ + private function getCmsBlockMock(int $entityId, string $identifier, string $content): BlockInterface + { + $cmsBlock = $this->createMock(BlockInterface::class); + + $cmsBlock->method('getId')->willReturn($entityId); + $cmsBlock->method('getIdentifier')->willReturn($identifier); + $cmsBlock->method('getContent')->willReturn($content); + + return $cmsBlock; + } + + /** + * Creates mock of the Filter that actually is doing nothing + * + * @return MockObject|Template + */ + private function getPassthroughFilterMock(): Template + { + $filterMock = $this->getMockBuilder(Template::class) + ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'filter']) + ->getMock(); + $filterMock->method('setStoreId')->willReturnSelf(); + $filterMock->method('filter')->willReturnArgument(0); + + return $filterMock; + } + + private function getCacheKeyStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string + { + return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId; + } + + private function getCacheKeyStubById(int $cmsBlockId): string + { + return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $cmsBlockId; + } +} From 450127126cb2c555c8bd2391e13bafdb7cf67922 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 02:55:44 +0200 Subject: [PATCH 006/195] Add support for save Block in the caches for all scopes that block is assigned to --- .../Magento/Cms/Block/BlockByIdentifier.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 11dbf03642289..e362806433404 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -14,7 +14,6 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\Context; -use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -83,7 +82,7 @@ protected function _toHtml(): string private function filterOutput(string $content): string { return $this->filterProvider->getBlockFilter() - ->setStoreId($this->getCurrentStore()->getId()) + ->setStoreId($this->getCurrentStoreId()) ->filter($content); } @@ -104,7 +103,7 @@ private function getCmsBlock(): BlockInterface if (null === $this->cmsBlock) { $this->cmsBlock = $this->blockByIdentifier->execute( (string)$this->getIdentifier(), - (int)$this->getCurrentStore()->getId() + $this->getCurrentStoreId() ); } @@ -112,14 +111,14 @@ private function getCmsBlock(): BlockInterface } /** - * Returns the StoreInterface of currently opened Store scope + * Returns the current Store ID * - * @return StoreInterface + * @return int * @throws \Magento\Framework\Exception\NoSuchEntityException */ - private function getCurrentStore(): StoreInterface + private function getCurrentStoreId(): int { - return $this->storeManager->getStore(); + return (int)$this->storeManager->getStore()->getId(); } /** @@ -133,10 +132,19 @@ private function getCurrentStore(): StoreInterface public function getIdentities(): array { try { - return [ - self::CACHE_KEY_PREFIX . '_' . $this->getCmsBlock()->getId(), - self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStore()->getId() - ]; + $cmsBlock = $this->getCmsBlock(); + + $identities = [self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId()]; + + if (method_exists($this->getCmsBlock(), 'getStores')) { + foreach ($cmsBlock->getStores() as $store) { + $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $store; + } + } + + $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId(); + + return $identities; } catch (NoSuchEntityException $e) { // If CMS Block does not exist, it should not be cached return []; From ae11a0c6908794a487129dc5b08e03d62025f2a9 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 03:21:51 +0200 Subject: [PATCH 007/195] Improve the logic behind getIdentifiers and Unit Tests coverage --- .../Magento/Cms/Block/BlockByIdentifier.php | 23 +++++++++++------- .../Test/Unit/Block/BlockByIdentifierTest.php | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index e362806433404..2f1ae518552fc 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -131,23 +131,28 @@ private function getCurrentStoreId(): int */ public function getIdentities(): array { + if (!$this->getIdentifier()) { + return []; + } + + $identities = [ + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier(), + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId() + ]; + try { $cmsBlock = $this->getCmsBlock(); - $identities = [self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId()]; + $identities[] = self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId(); if (method_exists($this->getCmsBlock(), 'getStores')) { - foreach ($cmsBlock->getStores() as $store) { - $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $store; + foreach ($cmsBlock->getStores() as $storeId) { + $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $storeId; } } - - $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId(); - - return $identities; + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { - // If CMS Block does not exist, it should not be cached - return []; } + return $identities; } } diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index b3e582be61446..3ff782fe728c9 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -14,6 +14,7 @@ use Magento\Cms\Model\Template\FilterProvider; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filter\Template; use Magento\Framework\View\Element\Context; use Magento\Store\Api\Data\StoreInterface; @@ -25,6 +26,7 @@ class BlockByIdentifierTest extends TestCase { private const STUB_MODULE_OUTPUT_DISABLED = false; private const STUB_EXISTING_IDENTIFIER = 'existingOne'; + private const STUB_UNAVAILABLE_IDENTIFIER = 'notExists'; private const STUB_DEFAULT_STORE = 1; private const STUB_CMS_BLOCK_ID = 1; private const STUB_CONTENT = 'Content'; @@ -32,6 +34,10 @@ class BlockByIdentifierTest extends TestCase private const ASSERT_EMPTY_BLOCK_HTML = ''; private const ASSERT_CONTENT_HTML = self::STUB_CONTENT; private const ASSERT_NO_CACHE_IDENTITIES = []; + private const ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES = [ + BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, + BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE + ]; /** @var MockObject|GetBlockByIdentifierInterface */ private $getBlockByIdentifierMock; @@ -61,12 +67,30 @@ public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void { // Given $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); // Expect $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); $this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities()); } + public function testBlockReturnsEmptyStringWhenIdentifierProvidedNotFound(): void + { + // Given + $this->getBlockByIdentifierMock->method('execute')->willThrowException( + new NoSuchEntityException(__('NoSuchEntityException')) + ); + $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(self::STUB_UNAVAILABLE_IDENTIFIER); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + + // Expect + $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); + $this->assertSame( + self::ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES, + $missingIdentifierBlock->getIdentities() + ); + } + public function testBlockReturnsCmsContentsWhenIdentifierFound(): void { // Given From 74b3e6228f65787dc7f514fbd8d49d9a672ebb8e Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 03:30:12 +0200 Subject: [PATCH 008/195] Rename stub methods and add missing doc blocks. --- .../Test/Unit/Block/BlockByIdentifierTest.php | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 3ff782fe728c9..2b0005c012f09 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -127,9 +127,9 @@ public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void $identities = $block->getIdentities(); // Then - $this->assertContains($this->getCacheKeyStubById(self::STUB_CMS_BLOCK_ID), $identities); + $this->assertContains($this->getIdentityStubById(self::STUB_CMS_BLOCK_ID), $identities); $this->assertContains( - $this->getCacheKeyStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), + $this->getIdentityStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), $identities ); } @@ -195,12 +195,25 @@ private function getPassthroughFilterMock(): Template return $filterMock; } - private function getCacheKeyStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string + /** + * Returns stub of Identity based on `$identifier` and `$storeId` + * + * @param string $identifier + * @param int $storeId + * @return string + */ + private function getIdentityStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string { return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId; } - private function getCacheKeyStubById(int $cmsBlockId): string + /** + * Returns stub of Identity based on `$cmsBlockId` + * + * @param int $cmsBlockId + * @return string + */ + private function getIdentityStubById(int $cmsBlockId): string { return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $cmsBlockId; } From 4656d9eaf44720445e3c40ba68acb782bbf6b809 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 03:31:49 +0200 Subject: [PATCH 009/195] Redundant empty line --- app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 2b0005c012f09..d6fb9e9ad1a44 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -1,5 +1,4 @@ Date: Fri, 8 May 2020 16:05:49 +0200 Subject: [PATCH 010/195] Final adjustments to the implementation --- .../Magento/Cms/Block/BlockByIdentifier.php | 20 ++++++++------ .../Test/Unit/Block/BlockByIdentifierTest.php | 27 ++++++++++++------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 2f1ae518552fc..eb14399741c41 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -9,6 +9,7 @@ use Magento\Cms\Api\Data\BlockInterface; use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Cms\Model\Block as BlockModel; use Magento\Cms\Model\Template\FilterProvider; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\NoSuchEntityException; @@ -45,6 +46,13 @@ class BlockByIdentifier extends AbstractBlock implements IdentityInterface */ private $cmsBlock; + /** + * @param GetBlockByIdentifierInterface $blockByIdentifier + * @param StoreManagerInterface $storeManager + * @param FilterProvider $filterProvider + * @param Context $context + * @param array $data + */ public function __construct( GetBlockByIdentifierInterface $blockByIdentifier, StoreManagerInterface $storeManager, @@ -89,7 +97,7 @@ private function filterOutput(string $content): string /** * Loads the CMS block by `identifier` provided as an argument * - * @return BlockInterface + * @return BlockInterface|BlockModel * @throws NoSuchEntityException */ private function getCmsBlock(): BlockInterface @@ -142,17 +150,13 @@ public function getIdentities(): array try { $cmsBlock = $this->getCmsBlock(); - - $identities[] = self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId(); - - if (method_exists($this->getCmsBlock(), 'getStores')) { - foreach ($cmsBlock->getStores() as $storeId) { - $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $storeId; - } + if ($cmsBlock instanceof IdentityInterface) { + $identities = array_merge($identities, $cmsBlock->getIdentities()); } // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } + return $identities; } } diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index d6fb9e9ad1a44..3a55666b2867b 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -10,6 +10,7 @@ use Magento\Cms\Api\Data\BlockInterface; use Magento\Cms\Api\GetBlockByIdentifierInterface; use Magento\Cms\Block\BlockByIdentifier; +use Magento\Cms\Model\Block; use Magento\Cms\Model\Template\FilterProvider; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\ManagerInterface; @@ -21,6 +22,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class BlockByIdentifierTest extends TestCase { private const STUB_MODULE_OUTPUT_DISABLED = false; @@ -37,6 +41,8 @@ class BlockByIdentifierTest extends TestCase BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE ]; + private const STUB_CMS_BLOCK_IDENTITY_BY_ID = 'CMS_BLOCK_' . self::STUB_CMS_BLOCK_ID; + private const STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER = 'CMS_BLOCK_' . self::STUB_EXISTING_IDENTIFIER; /** @var MockObject|GetBlockByIdentifierInterface */ private $getBlockByIdentifierMock; @@ -108,14 +114,19 @@ public function testBlockReturnsCmsContentsWhenIdentifierFound(): void $this->assertSame(self::ASSERT_CONTENT_HTML, $block->toHtml()); } - public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void + public function testBlockCacheIdentitiesContainCmsBlockIdentities(): void { // Given - $cmsBlockMock = $this->getCmsBlockMock( - self::STUB_CMS_BLOCK_ID, - self::STUB_EXISTING_IDENTIFIER, - self::STUB_CONTENT + $cmsBlockMock = $this->createMock(Block::class); + $cmsBlockMock->method('getId')->willReturn(self::STUB_CMS_BLOCK_ID); + $cmsBlockMock->method('getIdentifier')->willReturn(self::STUB_EXISTING_IDENTIFIER); + $cmsBlockMock->method('getIdentities')->willReturn( + [ + self::STUB_CMS_BLOCK_IDENTITY_BY_ID, + self::STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER + ] ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); $this->getBlockByIdentifierMock->method('execute') ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) @@ -127,10 +138,8 @@ public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void // Then $this->assertContains($this->getIdentityStubById(self::STUB_CMS_BLOCK_ID), $identities); - $this->assertContains( - $this->getIdentityStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), - $identities - ); + $this->assertContains(self::STUB_CMS_BLOCK_IDENTITY_BY_ID, $identities); + $this->assertContains(self::STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER, $identities); } /** From ab2be56ec292f0feee21c8f689603b8f28ea527a Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 16:56:52 +0200 Subject: [PATCH 011/195] Introduce private method to correctly support strict types --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index eb14399741c41..9bfad5c68c11a 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -19,8 +19,6 @@ /** * This class is replacement of \Magento\Cms\Block\Block, that accepts only `string` identifier of CMS Block - * - * @method getIdentifier(): int Returns the value of `identifier` injected in `` definition */ class BlockByIdentifier extends AbstractBlock implements IdentityInterface { @@ -80,6 +78,16 @@ protected function _toHtml(): string } } + /** + * Returns the value of `identifier` injected in `` definition + * + * @return string|null + */ + private function getIdentifier(): ?string + { + return $this->getdata('identifier') ?: null; + } + /** * Filters the Content * From ac5ef0772142af4a66ef1a7a4456d483d128483e Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 16:59:18 +0200 Subject: [PATCH 012/195] Cleaner way to handle ignoring the PHPCS warning --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 9bfad5c68c11a..690de727e8c05 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -161,7 +161,7 @@ public function getIdentities(): array if ($cmsBlock instanceof IdentityInterface) { $identities = array_merge($identities, $cmsBlock->getIdentities()); } - // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } From cdcad5c888cf752c1b8efb142d3f4716ebc772bd Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 23:23:39 +0200 Subject: [PATCH 013/195] Static check fixes --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- .../Cms/Test/Unit/Block/BlockByIdentifierTest.php | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 690de727e8c05..d579b3b222c53 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -22,7 +22,7 @@ */ class BlockByIdentifier extends AbstractBlock implements IdentityInterface { - const CACHE_KEY_PREFIX = 'CMS_BLOCK'; + public const CACHE_KEY_PREFIX = 'CMS_BLOCK'; /** * @var GetBlockByIdentifierInterface diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 3a55666b2867b..e9e94ffd966d0 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -203,18 +203,6 @@ private function getPassthroughFilterMock(): Template return $filterMock; } - /** - * Returns stub of Identity based on `$identifier` and `$storeId` - * - * @param string $identifier - * @param int $storeId - * @return string - */ - private function getIdentityStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string - { - return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId; - } - /** * Returns stub of Identity based on `$cmsBlockId` * From 5f3a2eb96bddda13363cf59284855259cc8aed71 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Sat, 16 May 2020 14:52:19 +0200 Subject: [PATCH 014/195] Fix invalid Unit Test declaration --- app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index e9e94ffd966d0..612d077110d9f 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -56,7 +56,7 @@ class BlockByIdentifierTest extends TestCase /** @var MockObject|StoreInterface */ private $storeMock; - protected function setUp() + protected function setUp(): void { $this->storeMock = $this->createMock(StoreInterface::class); $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); From 56b10a16d37751c518e5ded3fe34e4d6f9e8bb5b Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Thu, 21 May 2020 12:19:23 +0200 Subject: [PATCH 015/195] Change the type of Exception thrown when no Identifier provided --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index d579b3b222c53..7a51763176320 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -111,9 +111,7 @@ private function filterOutput(string $content): string private function getCmsBlock(): BlockInterface { if (!$this->getIdentifier()) { - throw new NoSuchEntityException( - __('Expected value of `identifier` was not provided') - ); + throw new \InvalidArgumentException('Expected value of `identifier` was not provided'); } if (null === $this->cmsBlock) { From 6e2f90bb4ae20d337eba85380560db5ac6d28d4b Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Thu, 21 May 2020 12:21:01 +0200 Subject: [PATCH 016/195] Adjust invalid Exception annotation --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 7a51763176320..caf7ca44261fe 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -106,7 +106,7 @@ private function filterOutput(string $content): string * Loads the CMS block by `identifier` provided as an argument * * @return BlockInterface|BlockModel - * @throws NoSuchEntityException + * @throws \InvalidArgumentException */ private function getCmsBlock(): BlockInterface { From 476a551b9f4cdb5691b3068718b6828bc6e106ef Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Thu, 21 May 2020 12:28:48 +0200 Subject: [PATCH 017/195] Add verification if Block is Enabled --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index caf7ca44261fe..464a002563323 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -107,6 +107,7 @@ private function filterOutput(string $content): string * * @return BlockInterface|BlockModel * @throws \InvalidArgumentException + * @throws NoSuchEntityException */ private function getCmsBlock(): BlockInterface { @@ -119,6 +120,12 @@ private function getCmsBlock(): BlockInterface (string)$this->getIdentifier(), $this->getCurrentStoreId() ); + + if (!$this->cmsBlock->isActive()) { + throw new NoSuchEntityException( + __('The CMS block with identifier "%identifier" is not enabled.', $this->getIdentifier()) + ); + } } return $this->cmsBlock; From d114bea5a9d50b36e8455689d0777a251be169a7 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Tue, 26 May 2020 22:43:27 +0200 Subject: [PATCH 018/195] Fix to Unit Tests after adjustments of `isActive` to implementation --- .../Test/Unit/Block/BlockByIdentifierTest.php | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 612d077110d9f..44b94b059cb6d 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -36,7 +36,6 @@ class BlockByIdentifierTest extends TestCase private const ASSERT_EMPTY_BLOCK_HTML = ''; private const ASSERT_CONTENT_HTML = self::STUB_CONTENT; - private const ASSERT_NO_CACHE_IDENTITIES = []; private const ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES = [ BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE @@ -68,15 +67,18 @@ protected function setUp(): void $this->filterProviderMock->method('getBlockFilter')->willReturn($this->getPassthroughFilterMock()); } - public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void + public function testBlockThrowsInvalidArgumentExceptionWhenNoIdentifierProvided(): void { // Given $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); // Expect - $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); - $this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities()); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected value of `identifier` was not provided'); + + // When + $missingIdentifierBlock->toHtml(); } public function testBlockReturnsEmptyStringWhenIdentifierProvidedNotFound(): void @@ -119,6 +121,7 @@ public function testBlockCacheIdentitiesContainCmsBlockIdentities(): void // Given $cmsBlockMock = $this->createMock(Block::class); $cmsBlockMock->method('getId')->willReturn(self::STUB_CMS_BLOCK_ID); + $cmsBlockMock->method('isActive')->willReturn(true); $cmsBlockMock->method('getIdentifier')->willReturn(self::STUB_EXISTING_IDENTIFIER); $cmsBlockMock->method('getIdentities')->willReturn( [ @@ -173,15 +176,21 @@ private function getTestedBlockUsingIdentifier(?string $identifier): BlockByIden * @param int $entityId * @param string $identifier * @param string $content + * @param bool $isActive * @return MockObject|BlockInterface */ - private function getCmsBlockMock(int $entityId, string $identifier, string $content): BlockInterface - { + private function getCmsBlockMock( + int $entityId, + string $identifier, + string $content, + bool $isActive = true + ): BlockInterface { $cmsBlock = $this->createMock(BlockInterface::class); $cmsBlock->method('getId')->willReturn($entityId); $cmsBlock->method('getIdentifier')->willReturn($identifier); $cmsBlock->method('getContent')->willReturn($content); + $cmsBlock->method('isActive')->willReturn($isActive); return $cmsBlock; } From 5e54bc441f4fd213eee6c1028c4e3ab9825d8e0b Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 12 Aug 2020 16:32:02 -0400 Subject: [PATCH 019/195] MC-24057: Implement static dependency analysis for wildcard and API urls - implemented checks for REST api urls - Added hard depdendency for Backend on CMS --- app/code/Magento/Backend/composer.json | 1 + .../TestFramework/Dependency/PhpRule.php | 87 ++++++++++++++----- .../Magento/Test/Integrity/DependencyTest.php | 6 +- 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index ee5491057d861..85b1d15a3368d 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -10,6 +10,7 @@ "magento/module-backup": "*", "magento/module-catalog": "*", "magento/module-config": "*", + "magento/module-cms": "*", "magento/module-customer": "*", "magento/module-developer": "*", "magento/module-directory": "*", diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 6fdeeb816f2cf..7ebecc0ece99e 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -15,6 +15,8 @@ use Magento\TestFramework\Dependency\Reader\ClassScanner; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; +use Magento\Webapi\Model\Config as WebApiConfig; +use Magento\Webapi\Model\Config\Converter; /** * Rule to check the dependencies between modules based on references, getUrl and layout blocks @@ -58,6 +60,12 @@ class PhpRule implements RuleInterface */ protected $_mapLayoutBlocks = []; + /** + * Used to retrieve information from WebApi urls + * @var WebApiConfig + */ + protected $webApiConfig; + /** * Default modules list. * @@ -88,21 +96,22 @@ class PhpRule implements RuleInterface /** * @param array $mapRouters * @param array $mapLayoutBlocks + * @param WebApiConfig $webApiConfig * @param array $pluginMap * @param array $whitelists * @param ClassScanner|null $classScanner - * - * @throws LocalizedException */ public function __construct( array $mapRouters, array $mapLayoutBlocks, + WebApiConfig $webApiConfig, array $pluginMap = [], array $whitelists = [], ClassScanner $classScanner = null ) { $this->_mapRouters = $mapRouters; $this->_mapLayoutBlocks = $mapLayoutBlocks; + $this->webApiConfig = $webApiConfig; $this->pluginMap = $pluginMap ?: null; $this->routeMapper = new RouteMapper(); $this->whitelists = $whitelists; @@ -297,36 +306,29 @@ private function isPluginDependency($dependent, $dependency) */ protected function _caseGetUrl(string $currentModule, string &$contents): array { - $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,}|\*)' - .'(/(?[a-z0-9\-_]+|\*))?(/(?[a-z0-9\-_]+|\*))?\3)#i'; - $dependencies = []; + $pattern = '#(\->|:)(?getUrl\([\'\"](?[a-zA-Z0-9\-_\*\/]+)[\'\"]\))#'; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { return $dependencies; } - try { foreach ($matches as $item) { - $routeId = $item['route_id']; - $controllerName = $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; - $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; - - // skip rest - if ($routeId === "rest") { //MC-19890 + $path = $item['path']; + $retDependencies = []; + if (preg_match('#rest(?/V1/\w+)#i', $path, $apiMatch)) { + $retDependencies = $this->processApiUrl($apiMatch['service']); + } elseif (strpos($path, "*") !== false) { + /** + * Skip processing wildcard urls since they always resolve to the current + * route_front_name/area_front_name/controller_name + */ continue; + } else { + $retDependencies = $this->processStandardUrl($path); } - // skip wildcards - if ($routeId === "*" || $controllerName === "*" || $actionName === "*") { //MC-19890 - continue; - } - $modules = $this->routeMapper->getDependencyByRoutePath( - $routeId, - $controllerName, - $actionName - ); - if (!in_array($currentModule, $modules)) { + if ($retDependencies && !in_array($currentModule, $retDependencies)) { $dependencies[] = [ - 'modules' => $modules, + 'modules' => $retDependencies, 'type' => RuleInterface::TYPE_HARD, 'source' => $item['source'], ]; @@ -337,10 +339,47 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e); } } - return $dependencies; } + /** + * @param string $path + * @return string[] + * @throws NoSuchActionException + */ + private function processStandardUrl(string $path) + { + $pattern = '#(?[a-z0-9\-_]{3,})' + . '\/?(?[a-z0-9\-_]+)?\/?(?[a-z0-9\-_]+)?#i'; + preg_match($pattern, $path, $match); + + $routeId = $match['route_id']; + $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; + $actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; + + return $this->routeMapper->getDependencyByRoutePath( + $routeId, + $controllerName, + $actionName + ); + } + + /** + * @param string $path + * @return string[] + */ + private function processApiUrl(string $path): array + { + $serviceMethods = $this->webApiConfig->getServices()[Converter::KEY_ROUTES][$path]; + + //assume that all HTTP methods use the same class + $method = $serviceMethods['GET'] ?? $serviceMethods['POST'] ?? $serviceMethods['PUT']; + $className = $method['service']['class']; + //get module from className + preg_match('#(?Magento(\\\\)\w+).*#', $className, $match); + return [$match['module']]; + } + /** * Check layout blocks * diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index d79e94af2bc01..6ec2482605672 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -14,14 +14,15 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; use Magento\Test\Integrity\Dependency\GraphQlSchemaDependencyProvider; +use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\DbRule; use Magento\TestFramework\Dependency\DiRule; use Magento\TestFramework\Dependency\LayoutRule; use Magento\TestFramework\Dependency\PhpRule; use Magento\TestFramework\Dependency\ReportsConfigRule; -use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; +use Magento\Webapi\Model\Config; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -279,11 +280,14 @@ protected static function _initRules() $tableToAnyModuleMap = self::getTableToAnyModuleMap(); // In case primary module declaring the table cannot be identified, use any module referencing this table $tableToModuleMap = array_merge($tableToAnyModuleMap, $tableToPrimaryModuleMap); + $objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager(); + $webApiConfig = $objectManager->create(Config::class); self::$_rulesInstances = [ new PhpRule( self::$routeMapper->getRoutes(), self::$_mapLayoutBlocks, + $webApiConfig, [], ['routes' => self::getRoutesWhitelist()] ), From 12337aad9a0e37b95acb4d32494f402ede78ee9f Mon Sep 17 00:00:00 2001 From: rrego6 Date: Thu, 20 Aug 2020 10:58:49 -0400 Subject: [PATCH 020/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Fixed Unit tests - Fixed style issues --- .../Magento/TestFramework/Dependency/PhpRule.php | 6 +++++- .../TestFramework/Dependency/PhpRuleTest.php | 13 ++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 7ebecc0ece99e..17242e0f5df88 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -317,7 +317,7 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array $retDependencies = []; if (preg_match('#rest(?/V1/\w+)#i', $path, $apiMatch)) { $retDependencies = $this->processApiUrl($apiMatch['service']); - } elseif (strpos($path, "*") !== false) { + } elseif (strpos($path, '*') !== false) { /** * Skip processing wildcard urls since they always resolve to the current * route_front_name/area_front_name/controller_name @@ -343,6 +343,8 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array } /** + * Helper method to get module dependencies used by a standard URL + * * @param string $path * @return string[] * @throws NoSuchActionException @@ -365,6 +367,8 @@ private function processStandardUrl(string $path) } /** + * Helper method to get module dependencies used by an API URL + * * @param string $path * @return string[] */ diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 3ddfbefaa346f..9973a7928397c 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -8,7 +8,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\TestFramework\Dependency\Reader\ClassScanner; -use Magento\TestFramework\Exception\NoSuchActionException; +use Magento\Webapi\Model\Config as WebApiConfig; /** * Test for PhpRule dependency check @@ -30,6 +30,11 @@ class PhpRuleTest extends \PHPUnit\Framework\TestCase */ private $classScanner; + /** + * @var WebApiConfig + */ + private $webApiConfig; + /** * @inheritDoc * @throws \Exception @@ -46,6 +51,7 @@ protected function setUp(): void $this->objectManagerHelper = new ObjectManagerHelper($this); $this->classScanner = $this->createMock(ClassScanner::class); + $this->webApiConfig = $this->objectManagerHelper->getObject(WebApiConfig::class); $this->model = $this->objectManagerHelper->getObject( PhpRule::class, @@ -53,6 +59,7 @@ protected function setUp(): void 'mapRouters' => $mapRoutes, 'mapLayoutBlocks' => $mapLayoutBlocks, 'pluginMap' => $pluginMap, + 'webApiConfig' => $this->webApiConfig, 'whitelists' => $whitelist, 'classScanner' => $this->classScanner ] @@ -253,7 +260,7 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() [ 'modules' => ['Magento\Cms'], 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, - 'source' => 'getUrl("cms/index/index"', + 'source' => 'getUrl("cms/index/index")', ] ] ], @@ -309,7 +316,7 @@ public function testGetDefaultModelDependency($module, $content, array $expected ], ], ]; - $this->model = new PhpRule([], $mapLayoutBlocks); + $this->model = new PhpRule([], $mapLayoutBlocks, $this->webApiConfig); $this->assertEquals($expected, $this->model->getDependencyInfo($module, 'template', 'any', $content)); } From 8fa2cfaf0598b2e5b9d32c8ee916b5df780a67c6 Mon Sep 17 00:00:00 2001 From: rrego6 Date: Tue, 25 Aug 2020 14:41:30 -0400 Subject: [PATCH 021/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Removed broken dependency, fixed tests --- .../TestFramework/Dependency/PhpRule.php | 33 +++++++++---------- .../TestFramework/Dependency/PhpRuleTest.php | 12 +++---- .../Magento/Test/Integrity/DependencyTest.php | 7 ++-- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 17242e0f5df88..2eaace4e70a32 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -15,7 +15,7 @@ use Magento\TestFramework\Dependency\Reader\ClassScanner; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; -use Magento\Webapi\Model\Config as WebApiConfig; +use Magento\Framework\Config\Reader\Filesystem as ConfigReader; use Magento\Webapi\Model\Config\Converter; /** @@ -62,9 +62,9 @@ class PhpRule implements RuleInterface /** * Used to retrieve information from WebApi urls - * @var WebApiConfig + * @var ConfigReader */ - protected $webApiConfig; + protected $configReader; /** * Default modules list. @@ -96,7 +96,7 @@ class PhpRule implements RuleInterface /** * @param array $mapRouters * @param array $mapLayoutBlocks - * @param WebApiConfig $webApiConfig + * @param ConfigReader $configReader * @param array $pluginMap * @param array $whitelists * @param ClassScanner|null $classScanner @@ -104,14 +104,14 @@ class PhpRule implements RuleInterface public function __construct( array $mapRouters, array $mapLayoutBlocks, - WebApiConfig $webApiConfig, + ConfigReader $configReader, array $pluginMap = [], array $whitelists = [], ClassScanner $classScanner = null ) { $this->_mapRouters = $mapRouters; $this->_mapLayoutBlocks = $mapLayoutBlocks; - $this->webApiConfig = $webApiConfig; + $this->configReader = $configReader; $this->pluginMap = $pluginMap ?: null; $this->routeMapper = new RouteMapper(); $this->whitelists = $whitelists; @@ -307,28 +307,28 @@ private function isPluginDependency($dependent, $dependency) protected function _caseGetUrl(string $currentModule, string &$contents): array { $dependencies = []; - $pattern = '#(\->|:)(?getUrl\([\'\"](?[a-zA-Z0-9\-_\*\/]+)[\'\"]\))#'; + $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-zA-Z0-9\-_*\/]+)\3)\s*[,)]#'; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { return $dependencies; } try { foreach ($matches as $item) { $path = $item['path']; - $retDependencies = []; - if (preg_match('#rest(?/V1/\w+)#i', $path, $apiMatch)) { - $retDependencies = $this->processApiUrl($apiMatch['service']); - } elseif (strpos($path, '*') !== false) { + $returnedDependencies = []; + if (strpos($path, '*') !== false) { /** * Skip processing wildcard urls since they always resolve to the current * route_front_name/area_front_name/controller_name */ continue; + } elseif (preg_match('#rest(?/V1/\w+)#i', $path, $apiMatch)) { + $returnedDependencies = $this->processApiUrl($apiMatch['service']); } else { - $retDependencies = $this->processStandardUrl($path); + $returnedDependencies = $this->processStandardUrl($path); } - if ($retDependencies && !in_array($currentModule, $retDependencies)) { + if ($returnedDependencies && !in_array($currentModule, $returnedDependencies)) { $dependencies[] = [ - 'modules' => $retDependencies, + 'modules' => $returnedDependencies, 'type' => RuleInterface::TYPE_HARD, 'source' => $item['source'], ]; @@ -374,10 +374,9 @@ private function processStandardUrl(string $path) */ private function processApiUrl(string $path): array { - $serviceMethods = $this->webApiConfig->getServices()[Converter::KEY_ROUTES][$path]; - + $serviceMethods = $this->configReader->read()[Converter::KEY_ROUTES][$path]; //assume that all HTTP methods use the same class - $method = $serviceMethods['GET'] ?? $serviceMethods['POST'] ?? $serviceMethods['PUT']; + $method = $serviceMethods['GET'] ?? $serviceMethods['POST'] ?? $serviceMethods['PUT'] ?? $serviceMethods['DELETE']; $className = $method['service']['class']; //get module from className preg_match('#(?Magento(\\\\)\w+).*#', $className, $match); diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 9973a7928397c..9d07853e1a06e 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -8,7 +8,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\TestFramework\Dependency\Reader\ClassScanner; -use Magento\Webapi\Model\Config as WebApiConfig; +use \Magento\Webapi\Model\Config\Reader; /** * Test for PhpRule dependency check @@ -31,9 +31,9 @@ class PhpRuleTest extends \PHPUnit\Framework\TestCase private $classScanner; /** - * @var WebApiConfig + * @var Reader */ - private $webApiConfig; + private $webApiConfigReader; /** * @inheritDoc @@ -51,7 +51,7 @@ protected function setUp(): void $this->objectManagerHelper = new ObjectManagerHelper($this); $this->classScanner = $this->createMock(ClassScanner::class); - $this->webApiConfig = $this->objectManagerHelper->getObject(WebApiConfig::class); + $this->webApiConfigReader = $this->objectManagerHelper->getObject(Reader::class); $this->model = $this->objectManagerHelper->getObject( PhpRule::class, @@ -59,7 +59,7 @@ protected function setUp(): void 'mapRouters' => $mapRoutes, 'mapLayoutBlocks' => $mapLayoutBlocks, 'pluginMap' => $pluginMap, - 'webApiConfig' => $this->webApiConfig, + 'webApiConfig' => $this->webApiConfigReader, 'whitelists' => $whitelist, 'classScanner' => $this->classScanner ] @@ -260,7 +260,7 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() [ 'modules' => ['Magento\Cms'], 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, - 'source' => 'getUrl("cms/index/index")', + 'source' => 'getUrl("cms/index/index"', ] ] ], diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 6ec2482605672..85bfb12835098 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -22,7 +22,7 @@ use Magento\TestFramework\Dependency\ReportsConfigRule; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; -use Magento\Webapi\Model\Config; +use \Magento\Webapi\Model\Config\Reader; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -281,13 +281,12 @@ protected static function _initRules() // In case primary module declaring the table cannot be identified, use any module referencing this table $tableToModuleMap = array_merge($tableToAnyModuleMap, $tableToPrimaryModuleMap); $objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager(); - $webApiConfig = $objectManager->create(Config::class); - + $webApiConfigReader = $objectManager->create(Reader::class); self::$_rulesInstances = [ new PhpRule( self::$routeMapper->getRoutes(), self::$_mapLayoutBlocks, - $webApiConfig, + $webApiConfigReader, [], ['routes' => self::getRoutesWhitelist()] ), From 66d1af560d5dee929d2d344fc257847cbcb12d7a Mon Sep 17 00:00:00 2001 From: rrego6 Date: Mon, 31 Aug 2020 06:43:59 -0400 Subject: [PATCH 022/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Removed webapi module dependencies - Fix api url logic --- .../TestFramework/Dependency/PhpRule.php | 41 +++++++++--- .../Test/Integrity/Dependency/Converter.php | 67 +++++++++++++++++++ .../Integrity/Dependency/SchemaLocator.php | 46 +++++++++++++ .../Dependency/WebapiFileResolver.php | 42 ++++++++++++ .../Magento/Test/Integrity/DependencyTest.php | 46 +++++++++++-- 5 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php create mode 100644 dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php create mode 100644 dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 2eaace4e70a32..dd8196464cb41 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -10,12 +10,12 @@ namespace Magento\TestFramework\Dependency; use Magento\Framework\App\Utility\Files; +use Magento\Framework\Config\Reader\Filesystem as ConfigReader; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\UrlInterface; use Magento\TestFramework\Dependency\Reader\ClassScanner; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; -use Magento\Framework\Config\Reader\Filesystem as ConfigReader; use Magento\Webapi\Model\Config\Converter; /** @@ -93,6 +93,11 @@ class PhpRule implements RuleInterface */ private $classScanner; + /** + * @var array + */ + private $serviceMethods; + /** * @param array $mapRouters * @param array $mapLayoutBlocks @@ -374,13 +379,33 @@ private function processStandardUrl(string $path) */ private function processApiUrl(string $path): array { - $serviceMethods = $this->configReader->read()[Converter::KEY_ROUTES][$path]; - //assume that all HTTP methods use the same class - $method = $serviceMethods['GET'] ?? $serviceMethods['POST'] ?? $serviceMethods['PUT'] ?? $serviceMethods['DELETE']; - $className = $method['service']['class']; - //get module from className - preg_match('#(?Magento(\\\\)\w+).*#', $className, $match); - return [$match['module']]; + if (!$this->serviceMethods) { + $this->serviceMethods = []; + $serviceRoutes = $this->configReader->read()[Converter::KEY_ROUTES]; + foreach ($serviceRoutes as $serviceRouteUrl => $methods) { + $pattern = '#:\w+#'; + $replace = '\w'; + $serviceRouteUrlRegex = $serviceRouteUrl; + preg_replace($pattern, $replace, $serviceRouteUrlRegex); + $serviceRouteUrlRegex = '#' . $serviceRouteUrlRegex . '#'; + $this->serviceMethods[$serviceRouteUrlRegex] = $methods; + } + } + foreach ($this->serviceMethods as $serviceMethodUrlRegex => $methods) { + //assume that all HTTP methods use the same class + if (preg_match($serviceMethodUrlRegex, $path)) { + $method = $methods['GET'] + ?? $methods['POST'] + ?? $methods['PUT'] + ?? $methods['DELETE']; + + $className = $method['service']['class']; + //get module from className + preg_match('#(?\w+[\\\]\w+).*#', $className, $match); + return [$match['module']]; + } + } + throw new NoSuchActionException(); } /** diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php new file mode 100644 index 0000000000000..0b22fc66e0012 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php @@ -0,0 +1,67 @@ +getElementsByTagName('route'); + /** @var \DOMElement $route */ + foreach ($routes as $route) { + if ($route->nodeType != XML_ELEMENT_NODE) { + continue; + } + /** @var \DOMElement $service */ + $service = $route->getElementsByTagName('service')->item(0); + $serviceClass = $service->attributes->getNamedItem('class')->nodeValue; + $serviceMethod = $service->attributes->getNamedItem('method')->nodeValue; + $url = trim($route->attributes->getNamedItem('url')->nodeValue); + + $method = $route->attributes->getNamedItem('method')->nodeValue; + + // We could handle merging here by checking if the route already exists + $result[self::KEY_ROUTES][$url][$method] = [ + self::KEY_SERVICE => [ + self::KEY_SERVICE_CLASS => $serviceClass, + self::KEY_SERVICE_METHOD => $serviceMethod, + ], + ]; + } + return $result; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php new file mode 100644 index 0000000000000..895c9124ad1b2 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php @@ -0,0 +1,46 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_Webapi'); + $this->schema = $module_path . '/etc/webapi_merged.xsd'; + $this->perFileSchema = $module_path . '/etc/webapi.xsd'; + } + + /** + * Return webapi_merged.xsd path + * + * @return string + */ + public function getSchema() + { + return $this->schema; + } + + /** + * Return webapi.xsd path + * + * @return string + */ + public function getPerFileSchema() + { + return $this->perFileSchema; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php new file mode 100644 index 0000000000000..32463bcf377d5 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php @@ -0,0 +1,42 @@ +componentRegistrar = $componentRegistrar; + } + + /** + * @inheritDoc + */ + public function get($filename, $scope) + { + if (!$this->webapixml_paths) { + $paths = $this->componentRegistrar->getPaths(ComponentRegistrar::MODULE); + $webapixml_paths = []; + foreach ($paths as $path) { + $path = $path . '/etc/webapi.xml'; + if (file_exists($path)) { + $webapixml_paths[$path] = file_get_contents($path); + } + } + $this->webapixml_paths = $webapixml_paths; + } + return $this->webapixml_paths; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 85bfb12835098..cbfda5909e88d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -11,9 +11,13 @@ use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Config\Reader\Filesystem as Reader; use Magento\Framework\Exception\LocalizedException; +use Magento\Test\Integrity\Dependency\Converter; use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; use Magento\Test\Integrity\Dependency\GraphQlSchemaDependencyProvider; +use Magento\Test\Integrity\Dependency\SchemaLocator; +use Magento\Test\Integrity\Dependency\WebapiFileResolver; use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\DbRule; use Magento\TestFramework\Dependency\DiRule; @@ -22,7 +26,6 @@ use Magento\TestFramework\Dependency\ReportsConfigRule; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; -use \Magento\Webapi\Model\Config\Reader; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -280,8 +283,28 @@ protected static function _initRules() $tableToAnyModuleMap = self::getTableToAnyModuleMap(); // In case primary module declaring the table cannot be identified, use any module referencing this table $tableToModuleMap = array_merge($tableToAnyModuleMap, $tableToPrimaryModuleMap); + $objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager(); - $webApiConfigReader = $objectManager->create(Reader::class); + + $webApiConfigReader = $objectManager->create( + Reader::class, + [ + 'fileResolver' => new WebapiFileResolver( + self::getComponentRegistrar() + ), + 'converter' => new Converter(), + 'schemaLocator' => new SchemaLocator( + self::getComponentRegistrar() + ), + 'fileName' => 'webapi.xml', + 'idAttributes' => [ + '/routes/route' => ['url', 'method'], + '/routes/route/resources/resource' => 'ref', + '/routes/route/data/parameter' => 'name' + ], + ] + ); + self::$_rulesInstances = [ new PhpRule( self::$routeMapper->getRoutes(), @@ -321,6 +344,17 @@ private static function getRoutesWhitelist(): array return self::$routesWhitelist; } + /** + * @return ComponentRegistrar + */ + private static function getComponentRegistrar() + { + if (!isset(self::$componentRegistrar)) { + self::$componentRegistrar = new ComponentRegistrar(); + } + return self::$componentRegistrar; + } + /** * Get full path to app/code directory, assuming these tests are run from the dev/tests directory. * @@ -559,18 +593,16 @@ function ($fileType, $file) use ($blackList) { */ private function getModuleNameForRelevantFile($file) { - if (!isset(self::$componentRegistrar)) { - self::$componentRegistrar = new ComponentRegistrar(); - } + $componentRegistrar = self::getComponentRegistrar(); // Validates file when it belongs to default themes - foreach (self::$componentRegistrar->getPaths(ComponentRegistrar::THEME) as $themeDir) { + foreach ($componentRegistrar->getPaths(ComponentRegistrar::THEME) as $themeDir) { if (strpos($file, $themeDir . '/') !== false) { return ''; } } $foundModuleName = ''; - foreach (self::$componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { if (strpos($file, $moduleDir . '/') !== false) { $foundModuleName = str_replace('_', '\\', $moduleName); break; From fdd21c9ed86ebf311ca3365165a2787ff81a872c Mon Sep 17 00:00:00 2001 From: rrego6 Date: Mon, 31 Aug 2020 08:30:17 -0400 Subject: [PATCH 023/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Fixed unit tests. - Fixed style --- .../TestFramework/Dependency/PhpRule.php | 37 ++++++++++++------- .../TestFramework/Dependency/PhpRuleTest.php | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index dd8196464cb41..cb842fc8f0151 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -358,17 +358,18 @@ private function processStandardUrl(string $path) { $pattern = '#(?[a-z0-9\-_]{3,})' . '\/?(?[a-z0-9\-_]+)?\/?(?[a-z0-9\-_]+)?#i'; - preg_match($pattern, $path, $match); - - $routeId = $match['route_id']; - $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; - $actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; - - return $this->routeMapper->getDependencyByRoutePath( - $routeId, - $controllerName, - $actionName - ); + if (preg_match($pattern, $path, $match)) { + $routeId = $match['route_id']; + $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; + $actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; + + return $this->routeMapper->getDependencyByRoutePath( + $routeId, + $controllerName, + $actionName + ); + } + throw new NoSuchActionException(); } /** @@ -379,6 +380,9 @@ private function processStandardUrl(string $path) */ private function processApiUrl(string $path): array { + /** + * Create regex patterns from service url paths + */ if (!$this->serviceMethods) { $this->serviceMethods = []; $serviceRoutes = $this->configReader->read()[Converter::KEY_ROUTES]; @@ -392,7 +396,10 @@ private function processApiUrl(string $path): array } } foreach ($this->serviceMethods as $serviceMethodUrlRegex => $methods) { - //assume that all HTTP methods use the same class + /** + * Since we expect that every service method should be within the same module, we can use the class from + * any method + */ if (preg_match($serviceMethodUrlRegex, $path)) { $method = $methods['GET'] ?? $methods['POST'] @@ -401,8 +408,10 @@ private function processApiUrl(string $path): array $className = $method['service']['class']; //get module from className - preg_match('#(?\w+[\\\]\w+).*#', $className, $match); - return [$match['module']]; + if (preg_match('#(?\w+[\\\]\w+).*#', $className, $match)) { + return [$match['module']]; + } + break; } } throw new NoSuchActionException(); diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 9d07853e1a06e..098372ce7be31 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -316,7 +316,7 @@ public function testGetDefaultModelDependency($module, $content, array $expected ], ], ]; - $this->model = new PhpRule([], $mapLayoutBlocks, $this->webApiConfig); + $this->model = new PhpRule([], $mapLayoutBlocks, $this->webApiConfigReader); $this->assertEquals($expected, $this->model->getDependencyInfo($module, 'template', 'any', $content)); } From 1b89c702b663d78a6eeb424c899bac2d3c2c4f36 Mon Sep 17 00:00:00 2001 From: rrego6 Date: Mon, 31 Aug 2020 08:46:27 -0400 Subject: [PATCH 024/195] MC-24057: Implement static dependency analysis for wildcard and API urls - removed useless annotations --- .../testsuite/Magento/Test/Integrity/Dependency/Converter.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php index 0b22fc66e0012..35224e50c0510 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php @@ -33,8 +33,6 @@ class Converter implements \Magento\Framework\Config\ConverterInterface /** * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function convert($source) { From 47978c28405a121338315d9da93997d9d6953392 Mon Sep 17 00:00:00 2001 From: rrego6 Date: Mon, 31 Aug 2020 10:30:49 -0400 Subject: [PATCH 025/195] MC-24057: Implement static dependency analysis for wildcard and API urls - removed useless annotations --- .../Magento/Test/Integrity/Dependency/WebapiFileResolver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php index 32463bcf377d5..8d424c37e465d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php @@ -1,5 +1,9 @@ Date: Mon, 31 Aug 2020 11:27:32 -0400 Subject: [PATCH 026/195] MC-24057: Implement static dependency analysis for wildcard and API urls - added copyright --- .../Magento/Test/Integrity/Dependency/SchemaLocator.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php index 895c9124ad1b2..2295d147c7366 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php @@ -1,5 +1,9 @@ Date: Tue, 1 Sep 2020 16:21:39 -0400 Subject: [PATCH 027/195] MC-24057: Implement static dependency analysis for wildcard and API urls - added implementation for checking controller wildcard urls --- .../TestFramework/Dependency/PhpRule.php | 76 ++++++++++++++++--- .../Test/Integrity/Dependency/Converter.php | 20 +---- .../Magento/Test/Integrity/DependencyTest.php | 29 +++---- 3 files changed, 82 insertions(+), 43 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index cb842fc8f0151..43cd2c12308f9 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -113,7 +113,8 @@ public function __construct( array $pluginMap = [], array $whitelists = [], ClassScanner $classScanner = null - ) { + ) + { $this->_mapRouters = $mapRouters; $this->_mapLayoutBlocks = $mapLayoutBlocks; $this->configReader = $configReader; @@ -146,7 +147,7 @@ public function getDependencyInfo($currentModule, $fileType, $file, &$contents) ); $dependenciesInfo = $this->considerCaseDependencies( $dependenciesInfo, - $this->_caseGetUrl($currentModule, $contents) + $this->_caseGetUrl($currentModule, $contents, $file) ); $dependenciesInfo = $this->considerCaseDependencies( $dependenciesInfo, @@ -304,12 +305,13 @@ private function isPluginDependency($dependent, $dependency) * * @param string $currentModule * @param string $contents + * @param string $file * @return array * @throws LocalizedException * @throws \Exception * @SuppressWarnings(PMD.CyclomaticComplexity) */ - protected function _caseGetUrl(string $currentModule, string &$contents): array + protected function _caseGetUrl(string $currentModule, string &$contents, string $file): array { $dependencies = []; $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-zA-Z0-9\-_*\/]+)\3)\s*[,)]#'; @@ -321,11 +323,7 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array $path = $item['path']; $returnedDependencies = []; if (strpos($path, '*') !== false) { - /** - * Skip processing wildcard urls since they always resolve to the current - * route_front_name/area_front_name/controller_name - */ - continue; + $returnedDependencies = $this->processWildcardUrl($path, $file); } elseif (preg_match('#rest(?/V1/\w+)#i', $path, $apiMatch)) { $returnedDependencies = $this->processApiUrl($apiMatch['service']); } else { @@ -347,6 +345,66 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array return $dependencies; } + /** + * Helper method to get module dependencies used by a wildcard Url + * + * @param string $urlPath + * @param string $filePath + * @return string[] + * @throws NoSuchActionException + */ + private function processWildcardUrl(string $urlPath, string $filePath) + { + $filePath = strtolower($filePath); + $urlRoutePieces = explode('/', $urlPath); + $routeId = array_shift($urlRoutePieces); + + //Skip route wildcard processing as this requires using the routeMapper + if ('*' === $routeId) { + return []; + } + $filePathInfo = pathinfo($filePath); + $fileActionName = $filePathInfo['filename']; + $filePathPieces = explode('/', $filePathInfo['dirname']); + + /** + * Only handle Controllers. ie: Ignore Blocks, Templates, and Models due to complexity in static resolution + * of route + */ + if (in_array('block', $filePathPieces) + || in_array('model', $filePathPieces) + || $filePathInfo['extension'] === 'phtml' + ) { + return []; + } + $fileControllerIndex = array_search('adminhtml', $filePathPieces) + ?? array_search('controller', $filePathPieces); + + $controllerName = array_shift($urlRoutePieces); + if ('*' === $controllerName) { + $fileControllerName = implode("_", array_slice($filePathPieces, $fileControllerIndex + 1)); + $controllerName = $fileControllerName; + } + + if (empty($urlRoutePieces) || !$urlRoutePieces[0]) { + return $this->routeMapper->getDependencyByRoutePath( + $routeId, + $controllerName, + UrlInterface::DEFAULT_ACTION_NAME + ); + } + + $actionName = array_shift($urlRoutePieces); + if ('*' === $actionName) { + $actionName = $fileActionName; + } + return $this->routeMapper->getDependencyByRoutePath( + $routeId, + $controllerName, + $actionName + ); + } + /** * Helper method to get module dependencies used by a standard URL * @@ -357,7 +415,7 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array private function processStandardUrl(string $path) { $pattern = '#(?[a-z0-9\-_]{3,})' - . '\/?(?[a-z0-9\-_]+)?\/?(?[a-z0-9\-_]+)?#i'; + . '\/?(?[a-z0-9\-_]+)?\/?(?[a-z0-9\-_]+)?#i'; if (preg_match($pattern, $path, $match)) { $routeId = $match['route_id']; $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php index 35224e50c0510..34f8c4a91f987 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php @@ -13,22 +13,10 @@ class Converter implements \Magento\Framework\Config\ConverterInterface /**#@+ * Array keys for config internal representation. */ - const KEY_SERVICE_CLASS = 'class'; - const KEY_URL = 'url'; - const KEY_SERVICE_METHOD = 'method'; - const KEY_SECURE = 'secure'; - const KEY_ROUTES = 'routes'; - const KEY_ACL_RESOURCES = 'resources'; - const KEY_SERVICE = 'service'; - const KEY_SERVICES = 'services'; - const KEY_FORCE = 'force'; - const KEY_VALUE = 'value'; - const KEY_DATA_PARAMETERS = 'parameters'; - const KEY_SOURCE = 'source'; - const KEY_METHOD = 'method'; - const KEY_METHODS = 'methods'; - const KEY_DESCRIPTION = 'description'; - const KEY_REAL_SERVICE_METHOD = 'realMethod'; + private const KEY_SERVICE_CLASS = 'class'; + private const KEY_SERVICE_METHOD = 'method'; + private const KEY_ROUTES = 'routes'; + private const KEY_SERVICE = 'service'; /**#@-*/ /** diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index cbfda5909e88d..76e63cc3f1988 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -26,6 +26,7 @@ use Magento\TestFramework\Dependency\ReportsConfigRule; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; +use Magento\TestFramework\Workaround\Override\Config\ValidationState; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -284,25 +285,17 @@ protected static function _initRules() // In case primary module declaring the table cannot be identified, use any module referencing this table $tableToModuleMap = array_merge($tableToAnyModuleMap, $tableToPrimaryModuleMap); - $objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager(); - - $webApiConfigReader = $objectManager->create( - Reader::class, + $webApiConfigReader = new Reader( + new WebapiFileResolver(self::getComponentRegistrar()), + new Converter(), + new SchemaLocator(self::getComponentRegistrar()), + new ValidationState(), + 'webapi.xml', [ - 'fileResolver' => new WebapiFileResolver( - self::getComponentRegistrar() - ), - 'converter' => new Converter(), - 'schemaLocator' => new SchemaLocator( - self::getComponentRegistrar() - ), - 'fileName' => 'webapi.xml', - 'idAttributes' => [ - '/routes/route' => ['url', 'method'], - '/routes/route/resources/resource' => 'ref', - '/routes/route/data/parameter' => 'name' - ], - ] + '/routes/route' => ['url', 'method'], + '/routes/route/resources/resource' => 'ref', + '/routes/route/data/parameter' => 'name' + ], ); self::$_rulesInstances = [ From 0444ee9648d5f6e1b831bfd2fe1993488d7cd15d Mon Sep 17 00:00:00 2001 From: rrego6 Date: Wed, 2 Sep 2020 10:24:30 -0400 Subject: [PATCH 028/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Refactor exceptions --- .../TestFramework/Dependency/PhpRule.php | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 43cd2c12308f9..27f9d0f6eb782 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -17,6 +17,7 @@ use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; use Magento\Webapi\Model\Config\Converter; +use PHPUnit\Framework\Exception; /** * Rule to check the dependencies between modules based on references, getUrl and layout blocks @@ -113,8 +114,7 @@ public function __construct( array $pluginMap = [], array $whitelists = [], ClassScanner $classScanner = null - ) - { + ) { $this->_mapRouters = $mapRouters; $this->_mapLayoutBlocks = $mapLayoutBlocks; $this->configReader = $configReader; @@ -416,18 +416,18 @@ private function processStandardUrl(string $path) { $pattern = '#(?[a-z0-9\-_]{3,})' . '\/?(?[a-z0-9\-_]+)?\/?(?[a-z0-9\-_]+)?#i'; - if (preg_match($pattern, $path, $match)) { - $routeId = $match['route_id']; - $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; - $actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; - - return $this->routeMapper->getDependencyByRoutePath( - $routeId, - $controllerName, - $actionName - ); + if (!preg_match($pattern, $path, $match)) { + throw new NoSuchActionException('Failed to parse standard url path: ' . $path); } - throw new NoSuchActionException(); + $routeId = $match['route_id']; + $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; + $actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; + + return $this->routeMapper->getDependencyByRoutePath( + $routeId, + $controllerName, + $actionName + ); } /** @@ -435,6 +435,8 @@ private function processStandardUrl(string $path) * * @param string $path * @return string[] + * + * @throws NoSuchActionException */ private function processApiUrl(string $path): array { @@ -459,20 +461,17 @@ private function processApiUrl(string $path): array * any method */ if (preg_match($serviceMethodUrlRegex, $path)) { - $method = $methods['GET'] - ?? $methods['POST'] - ?? $methods['PUT'] - ?? $methods['DELETE']; + $method = reset($methods); $className = $method['service']['class']; //get module from className if (preg_match('#(?\w+[\\\]\w+).*#', $className, $match)) { return [$match['module']]; } - break; + throw new Exception('Failed to parse class from className' . $className); } } - throw new NoSuchActionException(); + throw new NoSuchActionException('Failed to match service with url path: ' . $path); } /** From 61a75dda9284452d90c3e84d2409e05dc87f0faf Mon Sep 17 00:00:00 2001 From: rrego6 Date: Tue, 8 Sep 2020 08:07:49 -0400 Subject: [PATCH 029/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Added unit tests --- .../TestFramework/Dependency/PhpRule.php | 21 ++--- .../TestFramework/Dependency/PhpRuleTest.php | 89 ++++++++++++++++--- .../Dependency/WebapiFileResolver.php | 15 ++-- 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 27f9d0f6eb782..58d3a11064575 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -16,7 +16,7 @@ use Magento\TestFramework\Dependency\Reader\ClassScanner; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; -use Magento\Webapi\Model\Config\Converter; +use Magento\Test\Integrity\Dependency\Converter; use PHPUnit\Framework\Exception; /** @@ -324,7 +324,7 @@ protected function _caseGetUrl(string $currentModule, string &$contents, string $returnedDependencies = []; if (strpos($path, '*') !== false) { $returnedDependencies = $this->processWildcardUrl($path, $file); - } elseif (preg_match('#rest(?/V1/\w+)#i', $path, $apiMatch)) { + } elseif (preg_match('#rest(?/V1/.+)#i', $path, $apiMatch)) { $returnedDependencies = $this->processApiUrl($apiMatch['service']); } else { $returnedDependencies = $this->processStandardUrl($path); @@ -365,7 +365,7 @@ private function processWildcardUrl(string $urlPath, string $filePath) } $filePathInfo = pathinfo($filePath); $fileActionName = $filePathInfo['filename']; - $filePathPieces = explode('/', $filePathInfo['dirname']); + $filePathPieces = explode(DIRECTORY_SEPARATOR, $filePathInfo['dirname']); /** * Only handle Controllers. ie: Ignore Blocks, Templates, and Models due to complexity in static resolution @@ -377,8 +377,10 @@ private function processWildcardUrl(string $urlPath, string $filePath) ) { return []; } - $fileControllerIndex = array_search('adminhtml', $filePathPieces) - ?? array_search('controller', $filePathPieces); + $fileControllerIndex = array_search('adminhtml', $filePathPieces); + if (!$fileControllerIndex) { + $fileControllerIndex = array_search('controller', $filePathPieces); + } $controllerName = array_shift($urlRoutePieces); if ('*' === $controllerName) { @@ -445,22 +447,21 @@ private function processApiUrl(string $path): array */ if (!$this->serviceMethods) { $this->serviceMethods = []; - $serviceRoutes = $this->configReader->read()[Converter::KEY_ROUTES]; + $serviceRoutes = $this->configReader->read()['routes']; foreach ($serviceRoutes as $serviceRouteUrl => $methods) { $pattern = '#:\w+#'; $replace = '\w'; - $serviceRouteUrlRegex = $serviceRouteUrl; - preg_replace($pattern, $replace, $serviceRouteUrlRegex); + $serviceRouteUrlRegex = preg_replace($pattern, $replace, $serviceRouteUrl); $serviceRouteUrlRegex = '#' . $serviceRouteUrlRegex . '#'; $this->serviceMethods[$serviceRouteUrlRegex] = $methods; } } - foreach ($this->serviceMethods as $serviceMethodUrlRegex => $methods) { + foreach ($this->serviceMethods as $serviceRouteUrlRegex => $methods) { /** * Since we expect that every service method should be within the same module, we can use the class from * any method */ - if (preg_match($serviceMethodUrlRegex, $path)) { + if (preg_match($serviceRouteUrlRegex, $path)) { $method = reset($methods); $className = $method['service']['class']; diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 098372ce7be31..c2b0889b0b5d4 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -3,12 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\TestFramework\Dependency; +use Magento\Framework\Config\Reader\Filesystem; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\TestFramework\Dependency\Reader\ClassScanner; -use \Magento\Webapi\Model\Config\Reader; /** * Test for PhpRule dependency check @@ -31,7 +32,7 @@ class PhpRuleTest extends \PHPUnit\Framework\TestCase private $classScanner; /** - * @var Reader + * @var \PHPUnit\Framework\MockObject\MockObject | Filesystem */ private $webApiConfigReader; @@ -51,7 +52,7 @@ protected function setUp(): void $this->objectManagerHelper = new ObjectManagerHelper($this); $this->classScanner = $this->createMock(ClassScanner::class); - $this->webApiConfigReader = $this->objectManagerHelper->getObject(Reader::class); + $this->webApiConfigReader = $this->makeWebApiConfigReaderMock(); $this->model = $this->objectManagerHelper->getObject( PhpRule::class, @@ -59,7 +60,7 @@ protected function setUp(): void 'mapRouters' => $mapRoutes, 'mapLayoutBlocks' => $mapLayoutBlocks, 'pluginMap' => $pluginMap, - 'webApiConfig' => $this->webApiConfigReader, + 'configReader' => $this->webApiConfigReader, 'whitelists' => $whitelist, 'classScanner' => $this->classScanner ] @@ -114,7 +115,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento\SomeModule\Any\ClassName', ] ] @@ -132,7 +133,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento_SomeModule', ] ] @@ -150,7 +151,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento\SomeModule\Any\ClassName', ] ] @@ -168,7 +169,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'getBlock(\'block.name\')', ] ] @@ -192,7 +193,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\Module2'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_SOFT, + 'type' => RuleInterface::TYPE_SOFT, 'source' => 'Magento\Module2\Subject', ] ], @@ -204,7 +205,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\Module2'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_SOFT, + 'type' => RuleInterface::TYPE_SOFT, 'source' => 'Magento\Module2\NotSubject', ] ] @@ -216,7 +217,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\OtherModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento\OtherModule\NotSubject', ] ] @@ -259,11 +260,43 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() [ [ 'modules' => ['Magento\Cms'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'getUrl("cms/index/index"', ] ] ], + 'getUrl from API of same module' => [ + 'Magento\Catalog\SomeClass', + '$this->getUrl("rest/V1/products/3")', + [] + ], + 'getUrl from API of different module' => [ + 'Magento\Backend\SomeClass', + '$this->getUrl("rest/V1/products/43/options")', + [ + [ + 'modules' => ['Magento\Catalog'], + 'type' => RuleInterface::TYPE_HARD, + 'source' => 'getUrl("rest/V1/products/43/options"' + ] + ], + ], + //Skip processing routeid wildcards + 'getUrl from routeid wildcard in controller' => [ + 'Magento\Catalog\Controller\ControllerName\SomeClass', + '$this->getUrl("*/Invalid/*")', + [] + ], + 'getUrl from wildcard url within ignored Block class' => [ + 'Magento\Cms\Block\SomeClass', + '$this->getUrl("Catalog/*/View")', + [] + ], + 'getUrl from wildcard url for ControllerName' => [ + 'Magento\Catalog\Controller\Category\IGNORE', + '$this->getUrl("Catalog/*/View")', + [] + ], ]; } @@ -297,6 +330,11 @@ public function getDependencyInfoDataCaseGetUrlExceptionDataProvider() '$this->getUrl("someModule")', new LocalizedException(__('Invalid URL path: %1', 'somemodule/index/index')), ], + 'getUrl from unknown wildcard path' => [ + 'Magento\Catalog\Controller\Product\View', + '$this->getUrl("Catalog/*/INVALID")', + new LocalizedException(__('Invalid URL path: %1', 'catalog/product/invalid')), + ], ]; } @@ -332,7 +370,7 @@ public function getDefaultModelDependencyDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'getBlock(\'block.name\')', ] ], @@ -363,4 +401,29 @@ private function getModuleFromClass(string $class): string $moduleNameLength = strpos($class, '\\', strpos($class, '\\') + 1); return substr($class, 0, $moduleNameLength); } + + /** + * Returns an example list of services that would be parsed via the configReader + */ + private function makeWebApiConfigReaderMock() + { + $services = [ 'routes' => [ + '/V1/products/:sku' => [ + 'GET' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductRepositoryInterface', + 'method' => 'get' + ] ], + 'PUT' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductRepositoryInterface', + 'method' => 'save' + ] ], + ], + 'V1/products/:sku/options' => ['GET' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface', + 'method' => 'getList' + ] ] ] + ] ]; + + return $this->createConfiguredMock(Filesystem::class, [ 'read' => $services ]); + } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php index 8d424c37e465d..63b011f2c8a90 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php @@ -8,6 +8,9 @@ use Magento\Framework\Component\ComponentRegistrar; +/** + * Collects all webapi.xml files + */ class WebapiFileResolver implements \Magento\Framework\Config\FileResolverInterface { /** @@ -18,7 +21,7 @@ class WebapiFileResolver implements \Magento\Framework\Config\FileResolverInterf /** * @var string[] */ - private $webapixml_paths; + private $webapiXmlPaths; public function __construct(ComponentRegistrar $componentRegistrar) { @@ -30,17 +33,17 @@ public function __construct(ComponentRegistrar $componentRegistrar) */ public function get($filename, $scope) { - if (!$this->webapixml_paths) { + if (!$this->webapiXmlPaths) { $paths = $this->componentRegistrar->getPaths(ComponentRegistrar::MODULE); - $webapixml_paths = []; + $webapiXmlPaths = []; foreach ($paths as $path) { $path = $path . '/etc/webapi.xml'; if (file_exists($path)) { - $webapixml_paths[$path] = file_get_contents($path); + $webapiXmlPaths[$path] = file_get_contents($path); } } - $this->webapixml_paths = $webapixml_paths; + $this->webapiXmlPaths = $webapiXmlPaths; } - return $this->webapixml_paths; + return $this->webapiXmlPaths; } } From fc1599e615e9e90a7dd126b2ca4a4874303492be Mon Sep 17 00:00:00 2001 From: rrego6 Date: Tue, 8 Sep 2020 09:08:05 -0400 Subject: [PATCH 030/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Cylomatic complexity fix --- .../framework/Magento/TestFramework/Dependency/PhpRule.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 58d3a11064575..05b29692cb064 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -377,10 +377,8 @@ private function processWildcardUrl(string $urlPath, string $filePath) ) { return []; } - $fileControllerIndex = array_search('adminhtml', $filePathPieces); - if (!$fileControllerIndex) { - $fileControllerIndex = array_search('controller', $filePathPieces); - } + $fileControllerIndex = array_search('adminhtml', $filePathPieces) ?: + array_search('controller', $filePathPieces); $controllerName = array_shift($urlRoutePieces); if ('*' === $controllerName) { From caf03483a3b78963c2869c878358b82c12d0336b Mon Sep 17 00:00:00 2001 From: rrego6 Date: Tue, 8 Sep 2020 09:23:31 -0400 Subject: [PATCH 031/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Cylomatic complexity fix --- .../framework/Magento/TestFramework/Dependency/PhpRule.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 05b29692cb064..00e0c7c8382ae 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -352,6 +352,7 @@ protected function _caseGetUrl(string $currentModule, string &$contents, string * @param string $filePath * @return string[] * @throws NoSuchActionException + * @SuppressWarnings(PMD.CyclomaticComplexity) */ private function processWildcardUrl(string $urlPath, string $filePath) { @@ -377,8 +378,10 @@ private function processWildcardUrl(string $urlPath, string $filePath) ) { return []; } - $fileControllerIndex = array_search('adminhtml', $filePathPieces) ?: - array_search('controller', $filePathPieces); + $fileControllerIndex = array_search('adminhtml', $filePathPieces, true); + if ($fileControllerIndex === false) { + $fileControllerIndex = array_search('controller', $filePathPieces, true); + } $controllerName = array_shift($urlRoutePieces); if ('*' === $controllerName) { From 3487aff4175bdc5af289094503d9ef44f57723dc Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Tue, 8 Sep 2020 16:54:02 +0300 Subject: [PATCH 032/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/Configurable.php | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 6031ab6f8f8ae..2e0e7d82eb050 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -6,6 +6,7 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; +use Magento\Framework\DB\Select; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; @@ -140,6 +141,30 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); } + /** + * Filter select by inventory + * + * @param Select $select + * @return Select + */ + public function filterSelectByInventory(Select $select) + { + $select->join( + ['si' => $this->getTable('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->join( + ['si_parent' => $this->getTable('cataloginventory_stock_item')], + 'si_parent.product_id = l.parent_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); + + return $select; + } + /** * Apply configurable option * @@ -200,12 +225,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar // Does not make sense to extend query if out of stock products won't appear in tables for indexing if ($this->isConfigShowOutOfStock()) { - $select->join( - ['si' => $this->getTable('cataloginventory_stock_item')], - 'si.product_id = l.product_id', - [] - ); - $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select = $this->filterSelectByInventory($select); } $select->columns( From 732be76809df59372499a0ee6931242c539353bb Mon Sep 17 00:00:00 2001 From: rrego6 Date: Tue, 8 Sep 2020 10:32:54 -0400 Subject: [PATCH 033/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Fixed comments --- .../Magento/TestFramework/Dependency/PhpRuleTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index c2b0889b0b5d4..2cafd5fcc9344 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -281,7 +281,7 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() ] ], ], - //Skip processing routeid wildcards + //Skip processing routeid wildcards due to complexity in resolution 'getUrl from routeid wildcard in controller' => [ 'Magento\Catalog\Controller\ControllerName\SomeClass', '$this->getUrl("*/Invalid/*")', @@ -404,6 +404,8 @@ private function getModuleFromClass(string $class): string /** * Returns an example list of services that would be parsed via the configReader + * + * @return \PHPUnit\Framework\MockObject\MockObject | Filesystem */ private function makeWebApiConfigReaderMock() { From 87f5b98dc7d556f2662c75238f7a4267637ac0a7 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Wed, 9 Sep 2020 17:12:09 +0300 Subject: [PATCH 034/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Indexer/Price/ConfigurableTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index ffa84ca740e62..6c0a92372ae3d 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -157,6 +159,41 @@ public function testReindexWithCorrectPriority() $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } + /** + * Test get product minimal price if all children is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testReindexIfAllChildrenIsOutOfStock(): void + { + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct1 = $this->productRepository->getById(10, false, null, true); + $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $childProduct2 = $this->productRepository->getById(20, false, null, true); + $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); + $priceIndexerProcessor->reindexList( + [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], + true + ); + + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php From be401c034202e51be9da92029eaf1d08a119fc6c Mon Sep 17 00:00:00 2001 From: rrego6 Date: Wed, 9 Sep 2020 15:29:26 -0400 Subject: [PATCH 035/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Refactored code as per CR suggestions --- .../TestFramework/Dependency/PhpRule.php | 95 +++++++++---------- .../TestFramework/Dependency/PhpRuleTest.php | 8 +- .../Test/Integrity/Dependency/Converter.php | 22 +++-- 3 files changed, 62 insertions(+), 63 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 00e0c7c8382ae..53401f0b47f04 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -11,13 +11,13 @@ use Magento\Framework\App\Utility\Files; use Magento\Framework\Config\Reader\Filesystem as ConfigReader; +use Magento\Framework\Exception\ConfigurationMismatchException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\UrlInterface; use Magento\TestFramework\Dependency\Reader\ClassScanner; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; -use Magento\Test\Integrity\Dependency\Converter; -use PHPUnit\Framework\Exception; +use Magento\TestFramework\Inspection\Exception; /** * Rule to check the dependencies between modules based on references, getUrl and layout blocks @@ -308,30 +308,28 @@ private function isPluginDependency($dependent, $dependency) * @param string $file * @return array * @throws LocalizedException - * @throws \Exception - * @SuppressWarnings(PMD.CyclomaticComplexity) */ protected function _caseGetUrl(string $currentModule, string &$contents, string $file): array { $dependencies = []; - $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-zA-Z0-9\-_*\/]+)\3)\s*[,)]#'; + $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-zA-Z0-9\-_*/]+)\3)\s*[,)]#'; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { return $dependencies; } try { foreach ($matches as $item) { $path = $item['path']; - $returnedDependencies = []; + $modules = []; if (strpos($path, '*') !== false) { - $returnedDependencies = $this->processWildcardUrl($path, $file); + $modules = $this->processWildcardUrl($path, $file); } elseif (preg_match('#rest(?/V1/.+)#i', $path, $apiMatch)) { - $returnedDependencies = $this->processApiUrl($apiMatch['service']); + $modules = $this->processApiUrl($apiMatch['service']); } else { - $returnedDependencies = $this->processStandardUrl($path); + $modules = $this->processStandardUrl($path); } - if ($returnedDependencies && !in_array($currentModule, $returnedDependencies)) { + if ($modules && !in_array($currentModule, $modules)) { $dependencies[] = [ - 'modules' => $returnedDependencies, + 'modules' => $modules, 'type' => RuleInterface::TYPE_HARD, 'source' => $item['source'], ]; @@ -352,55 +350,43 @@ protected function _caseGetUrl(string $currentModule, string &$contents, string * @param string $filePath * @return string[] * @throws NoSuchActionException - * @SuppressWarnings(PMD.CyclomaticComplexity) */ private function processWildcardUrl(string $urlPath, string $filePath) { $filePath = strtolower($filePath); $urlRoutePieces = explode('/', $urlPath); $routeId = array_shift($urlRoutePieces); - //Skip route wildcard processing as this requires using the routeMapper if ('*' === $routeId) { return []; } - $filePathInfo = pathinfo($filePath); - $fileActionName = $filePathInfo['filename']; - $filePathPieces = explode(DIRECTORY_SEPARATOR, $filePathInfo['dirname']); /** * Only handle Controllers. ie: Ignore Blocks, Templates, and Models due to complexity in static resolution * of route */ - if (in_array('block', $filePathPieces) - || in_array('model', $filePathPieces) - || $filePathInfo['extension'] === 'phtml' - ) { + if (!preg_match( + '#controller/(adminhtml/)?(?.+)/(?\w+).php$#', + $filePath, + $fileParts + )) { return []; } - $fileControllerIndex = array_search('adminhtml', $filePathPieces, true); - if ($fileControllerIndex === false) { - $fileControllerIndex = array_search('controller', $filePathPieces, true); - } $controllerName = array_shift($urlRoutePieces); if ('*' === $controllerName) { - $fileControllerName = implode("_", array_slice($filePathPieces, $fileControllerIndex + 1)); - $controllerName = $fileControllerName; + $controllerName = str_replace('/', '_', $fileParts['controller_name']); } if (empty($urlRoutePieces) || !$urlRoutePieces[0]) { - return $this->routeMapper->getDependencyByRoutePath( - $routeId, - $controllerName, - UrlInterface::DEFAULT_ACTION_NAME - ); + $actionName = UrlInterface::DEFAULT_ACTION_NAME; + } else { + $actionName = array_shift($urlRoutePieces); + if ('*' === $actionName) { + $actionName = $fileParts['action_name']; + } } - $actionName = array_shift($urlRoutePieces); - if ('*' === $actionName) { - $actionName = $fileActionName; - } return $this->routeMapper->getDependencyByRoutePath( $routeId, $controllerName, @@ -418,7 +404,7 @@ private function processWildcardUrl(string $urlPath, string $filePath) private function processStandardUrl(string $path) { $pattern = '#(?[a-z0-9\-_]{3,})' - . '\/?(?[a-z0-9\-_]+)?\/?(?[a-z0-9\-_]+)?#i'; + . '(/(?[a-z0-9\-_]+))?(/(?[a-z0-9\-_]+))?#i'; if (!preg_match($pattern, $path, $match)) { throw new NoSuchActionException('Failed to parse standard url path: ' . $path); } @@ -434,30 +420,37 @@ private function processStandardUrl(string $path) } /** - * Helper method to get module dependencies used by an API URL - * - * @param string $path - * @return string[] - * - * @throws NoSuchActionException + * Create regex patterns from service url paths + * @return array */ - private function processApiUrl(string $path): array + private function getServiceMethodRegexps(): array { - /** - * Create regex patterns from service url paths - */ if (!$this->serviceMethods) { $this->serviceMethods = []; $serviceRoutes = $this->configReader->read()['routes']; foreach ($serviceRoutes as $serviceRouteUrl => $methods) { $pattern = '#:\w+#'; - $replace = '\w'; + $replace = '\w+'; $serviceRouteUrlRegex = preg_replace($pattern, $replace, $serviceRouteUrl); - $serviceRouteUrlRegex = '#' . $serviceRouteUrlRegex . '#'; + $serviceRouteUrlRegex = '#^' . $serviceRouteUrlRegex . '$#'; $this->serviceMethods[$serviceRouteUrlRegex] = $methods; } } - foreach ($this->serviceMethods as $serviceRouteUrlRegex => $methods) { + return $this->serviceMethods; + } + + /** + * Helper method to get module dependencies used by an API URL + * + * @param string $path + * @return string[] + * + * @throws NoSuchActionException + * @throws Exception + */ + private function processApiUrl(string $path): array + { + foreach ($this->getServiceMethodRegexps() as $serviceRouteUrlRegex => $methods) { /** * Since we expect that every service method should be within the same module, we can use the class from * any method @@ -467,10 +460,10 @@ private function processApiUrl(string $path): array $className = $method['service']['class']; //get module from className - if (preg_match('#(?\w+[\\\]\w+).*#', $className, $match)) { + if (preg_match('#^(?\w+[\\\]\w+)#', $className, $match)) { return [$match['module']]; } - throw new Exception('Failed to parse class from className' . $className); + throw new Exception('Failed to parse class from className: ' . $className); } } throw new NoSuchActionException('Failed to match service with url path: ' . $path); diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 2cafd5fcc9344..74c8bd97c7eb1 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -281,7 +281,6 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() ] ], ], - //Skip processing routeid wildcards due to complexity in resolution 'getUrl from routeid wildcard in controller' => [ 'Magento\Catalog\Controller\ControllerName\SomeClass', '$this->getUrl("*/Invalid/*")', @@ -292,6 +291,11 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() '$this->getUrl("Catalog/*/View")', [] ], + 'getUrl from wildcard url within ignored Block class' => [ + 'Magento\Cms\Block\SomeClass', + '$this->getUrl("Catalog/*/View")', + [] + ], 'getUrl from wildcard url for ControllerName' => [ 'Magento\Catalog\Controller\Category\IGNORE', '$this->getUrl("Catalog/*/View")', @@ -420,7 +424,7 @@ private function makeWebApiConfigReaderMock() 'method' => 'save' ] ], ], - 'V1/products/:sku/options' => ['GET' => ['service' => [ + '/V1/products/:sku/options' => ['GET' => ['service' => [ 'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface', 'method' => 'getList' ] ] ] diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php index 34f8c4a91f987..226512a7a40ed 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php @@ -13,8 +13,10 @@ class Converter implements \Magento\Framework\Config\ConverterInterface /**#@+ * Array keys for config internal representation. */ - private const KEY_SERVICE_CLASS = 'class'; - private const KEY_SERVICE_METHOD = 'method'; + private const KEY_URL = 'url'; + private const KEY_CLASS = 'class'; + private const KEY_METHOD = 'method'; + private const KEY_ROUTE = 'route'; private const KEY_ROUTES = 'routes'; private const KEY_SERVICE = 'service'; /**#@-*/ @@ -26,25 +28,25 @@ public function convert($source) { $result = []; /** @var \DOMNodeList $routes */ - $routes = $source->getElementsByTagName('route'); + $routes = $source->getElementsByTagName(self::KEY_ROUTE); /** @var \DOMElement $route */ foreach ($routes as $route) { if ($route->nodeType != XML_ELEMENT_NODE) { continue; } /** @var \DOMElement $service */ - $service = $route->getElementsByTagName('service')->item(0); - $serviceClass = $service->attributes->getNamedItem('class')->nodeValue; - $serviceMethod = $service->attributes->getNamedItem('method')->nodeValue; - $url = trim($route->attributes->getNamedItem('url')->nodeValue); + $service = $route->getElementsByTagName(self::KEY_SERVICE)->item(0); + $serviceClass = $service->attributes->getNamedItem(self::KEY_CLASS)->nodeValue; + $serviceMethod = $service->attributes->getNamedItem(self::KEY_METHOD)->nodeValue; + $url = trim($route->attributes->getNamedItem(self::KEY_URL)->nodeValue); - $method = $route->attributes->getNamedItem('method')->nodeValue; + $method = $route->attributes->getNamedItem(self::KEY_METHOD)->nodeValue; // We could handle merging here by checking if the route already exists $result[self::KEY_ROUTES][$url][$method] = [ self::KEY_SERVICE => [ - self::KEY_SERVICE_CLASS => $serviceClass, - self::KEY_SERVICE_METHOD => $serviceMethod, + self::KEY_CLASS => $serviceClass, + self::KEY_SERVICE => $serviceMethod, ], ]; } From 7442aece17d4f3d5e341595b060babc2a8ae112d Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Thu, 10 Sep 2020 10:58:12 +0300 Subject: [PATCH 036/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/ConfigurableTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index 6c0a92372ae3d..700746ebbd3ad 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -155,7 +155,7 @@ public function testReindexWithCorrectPriority() true ); - $configurableProduct = $this->getConfigurableProductFromCollection($configurableProduct->getId()); + $configurableProduct = $this->getConfigurableProductFromCollection((int)$configurableProduct->getId()); $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } @@ -184,6 +184,11 @@ public function testReindexIfAllChildrenIsOutOfStock(): void $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); $this->stockRepository->save($stockItem); + $configurableProduct1 = $this->productRepository->getById(1, false, null, true); + $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); $priceIndexerProcessor->reindexList( [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], From 7ee934df7e141f0b9285fc8ec3f1532e6f8f95ef Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Thu, 10 Sep 2020 12:19:10 +0300 Subject: [PATCH 037/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/Configurable.php | 36 +++++----- .../Price/StockStatusBaseSelectProcessor.php | 68 +++++++++++++++++++ .../Magento/ConfigurableProduct/etc/di.xml | 1 + 3 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 2e0e7d82eb050..903aa837f2d3b 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; use Magento\Framework\DB\Select; use Magento\Framework\Indexer\DimensionalIndexerInterface; @@ -14,10 +17,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; use Magento\CatalogInventory\Model\Stock; -use Magento\CatalogInventory\Model\Configuration; /** * Configurable Products Price Indexer Resource model @@ -76,6 +77,11 @@ class Configurable implements DimensionalIndexerInterface */ private $scopeConfig; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param BaseFinalPrice $baseFinalPrice * @param IndexTableStructureFactory $indexTableStructureFactory @@ -86,6 +92,9 @@ class Configurable implements DimensionalIndexerInterface * @param bool $fullReindexAction * @param string $connectionName * @param ScopeConfigInterface $scopeConfig + * @param BaseSelectProcessorInterface|null $baseSelectProcessor + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( BaseFinalPrice $baseFinalPrice, @@ -96,7 +105,8 @@ public function __construct( BasePriceModifier $basePriceModifier, $fullReindexAction = false, $connectionName = 'indexer', - ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + ?BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->baseFinalPrice = $baseFinalPrice; $this->indexTableStructureFactory = $indexTableStructureFactory; @@ -107,6 +117,8 @@ public function __construct( $this->fullReindexAction = $fullReindexAction; $this->basePriceModifier = $basePriceModifier; $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->baseSelectProcessor = $baseSelectProcessor ?: + ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -223,10 +235,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar [] ); - // Does not make sense to extend query if out of stock products won't appear in tables for indexing - if ($this->isConfigShowOutOfStock()) { - $select = $this->filterSelectByInventory($select); - } + $this->baseSelectProcessor->process($select); $select->columns( [ @@ -315,17 +324,4 @@ private function getTable($tableName) { return $this->resource->getTableName($tableName, $this->connectionName); } - - /** - * Is flag Show Out Of Stock setted - * - * @return bool - */ - private function isConfigShowOutOfStock(): bool - { - return $this->scopeConfig->isSetFlag( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - ScopeInterface::SCOPE_STORE - ); - } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php new file mode 100644 index 0000000000000..bd4a5dd5fa39a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php @@ -0,0 +1,68 @@ +resource = $resource; + $this->stockConfig = $stockConfig; + } + + /** + * {@inheritdoc} + */ + public function process(Select $select) + { + // Does not make sense to extend query if out of stock products won't appear in tables for indexing + if ($this->stockConfig->isShowOutOfStock()) { + $select->join( + ['si' => $this->resource->getTableName('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->join( + ['si_parent' => $this->resource->getTableName('cataloginventory_stock_item')], + 'si_parent.product_id = l.parent_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); + } + + return $select; + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index c8a278df92dc6..9f01af66f9713 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -198,6 +198,7 @@ Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy indexer + Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\StockStatusBaseSelectProcessor From 4ec639803c33d0d020c40ec20fdf41083b764400 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Thu, 10 Sep 2020 15:59:47 +0300 Subject: [PATCH 038/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Indexer/Price/ConfigurableTest.php | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index 700746ebbd3ad..ba3d5e46b98fb 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -159,46 +159,6 @@ public function testReindexWithCorrectPriority() $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } - /** - * Test get product minimal price if all children is out of stock - * - * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoDbIsolation disabled - * - * @return void - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function testReindexIfAllChildrenIsOutOfStock(): void - { - $configurableProduct = $this->getConfigurableProductFromCollection(1); - $this->assertEquals(10, $configurableProduct->getMinimalPrice()); - - $childProduct1 = $this->productRepository->getById(10, false, null, true); - $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); - $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); - $this->stockRepository->save($stockItem); - - $childProduct2 = $this->productRepository->getById(20, false, null, true); - $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); - $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); - $this->stockRepository->save($stockItem); - - $configurableProduct1 = $this->productRepository->getById(1, false, null, true); - $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); - $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); - $this->stockRepository->save($stockItem); - - $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); - $priceIndexerProcessor->reindexList( - [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], - true - ); - - $configurableProduct = $this->getConfigurableProductFromCollection(1); - $this->assertEquals(10, $configurableProduct->getMinimalPrice()); - } - /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php From f5d690232c5fc9fd163844cddc1dc8b43811fb93 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Fri, 11 Sep 2020 10:23:31 +0300 Subject: [PATCH 039/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/Configurable.php | 24 ------------------- .../Price/StockStatusBaseSelectProcessor.php | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 903aa837f2d3b..d00e5c72a4622 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -153,30 +153,6 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); } - /** - * Filter select by inventory - * - * @param Select $select - * @return Select - */ - public function filterSelectByInventory(Select $select) - { - $select->join( - ['si' => $this->getTable('cataloginventory_stock_item')], - 'si.product_id = l.product_id', - [] - ); - $select->join( - ['si_parent' => $this->getTable('cataloginventory_stock_item')], - 'si_parent.product_id = l.parent_id', - [] - ); - $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); - $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); - - return $select; - } - /** * Apply configurable option * diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php index bd4a5dd5fa39a..b5cbaa57858c9 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php @@ -43,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function process(Select $select) { From a44b2000b7a590ecb4dc30190a5595c4a6106e3f Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 14 Sep 2020 17:52:48 +0300 Subject: [PATCH 040/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Indexer/Price/ConfigurableTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index ba3d5e46b98fb..28cbf80703d51 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -159,6 +159,40 @@ public function testReindexWithCorrectPriority() $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } + /** + * Test get product minimal price if all children is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testReindexIfAllChildrenIsOutOfStock(): void + { + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct1 = $this->productRepository->getById(10, false, null, true); + $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $childProduct2 = $this->productRepository->getById(20, false, null, true); + $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct1 = $this->productRepository->getById(1, false, null, true); + $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php From 893910efd99c804f03af07ee489f18222e28c0e5 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Tue, 15 Sep 2020 11:36:24 +0300 Subject: [PATCH 041/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Magento/Catalog/Block/Product/ListProduct/SortingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php index 52e2047917e8e..b88edc656176c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php @@ -439,7 +439,7 @@ public function productListWithOutOfStockSortOrderDataProvider(): array 'default_order_price_desc' => [ 'sort' => 'price', 'direction' => Collection::SORT_ORDER_DESC, - 'expectation' => ['simple3', 'simple2', 'simple1', 'configurable'], + 'expectation' => ['configurable', 'simple3', 'simple2', 'simple1'], ], ]; } From a17053a0450f0469d044e2b63cd520bc49593ec8 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk Date: Wed, 23 Sep 2020 09:38:11 +0300 Subject: [PATCH 042/195] Use keyCode const to check if forward slash was pressed --- app/code/Magento/Ui/view/base/web/js/lib/key-codes.js | 1 + app/design/adminhtml/Magento/backend/web/js/theme.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js b/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js index 1f5a4210793ba..1f25e0d2c089f 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js @@ -21,6 +21,7 @@ define([], function () { 17: 'ctrlKey', 18: 'altKey', 16: 'shiftKey', + 191: 'forwardSlashKey', 66: 'bKey', 73: 'iKey', 85: 'uKey' diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 996f6c05935f2..f556a388f6cf0 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -312,8 +312,9 @@ define('globalNavigation', [ define('globalSearch', [ 'jquery', + 'Magento_Ui/js/lib/key-codes', 'jquery/ui' -], function ($) { +], function ($, keyCodes) { 'use strict'; $.widget('mage.globalSearch', { @@ -353,7 +354,7 @@ define('globalSearch', [ 'textarea' ]; - if (e.which !== 191 || // forward slash - '/' + if (keyCodes[e.which] !== 'forwardSlashKey' || inputs.indexOf(e.target.tagName.toLowerCase()) !== -1 || e.target.isContentEditable ) { From d03302151a75807da0c16e2e00ced665760f9845 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk Date: Wed, 23 Sep 2020 09:39:43 +0300 Subject: [PATCH 043/195] Rename 'e' into 'event' --- app/design/adminhtml/Magento/backend/web/js/theme.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index f556a388f6cf0..e2b8d8cfc884d 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -347,21 +347,21 @@ define('globalSearch', [ self.field.addClass(self.options.fieldActiveClass); }); - $(document).keydown(function (e) { + $(document).keydown(function (event) { var inputs = [ 'input', 'select', 'textarea' ]; - if (keyCodes[e.which] !== 'forwardSlashKey' || - inputs.indexOf(e.target.tagName.toLowerCase()) !== -1 || - e.target.isContentEditable + if (keyCodes[event.which] !== 'forwardSlashKey' || + inputs.indexOf(event.target.tagName.toLowerCase()) !== -1 || + event.target.isContentEditable ) { return; } - e.preventDefault(); + event.preventDefault(); self.input.focus(); }); From e44b4868c0a93c863b511f347a868e731e2b3102 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Wed, 23 Sep 2020 14:19:33 -0500 Subject: [PATCH 044/195] [AWS S3] MC-37452: Introduce new Adapter (#6166) MC-37532: Introduce new module with adapter --- app/code/Magento/AwsS3/Driver/AwsS3.php | 553 +++++++ .../Magento/AwsS3/Driver/AwsS3Factory.php | 47 + app/code/Magento/AwsS3/LICENSE.txt | 48 + app/code/Magento/AwsS3/LICENSE_AFL.txt | 48 + app/code/Magento/AwsS3/Model/Config.php | 75 + app/code/Magento/AwsS3/README.md | 3 + app/code/Magento/AwsS3/composer.json | 23 + app/code/Magento/AwsS3/etc/adminhtml/di.xml | 19 + .../Magento/AwsS3/etc/adminhtml/system.xml | 35 + app/code/Magento/AwsS3/etc/di.xml | 16 + app/code/Magento/AwsS3/etc/module.xml | 14 + app/code/Magento/AwsS3/registration.php | 9 + .../Reader/Source/Deployed/DocumentRoot.php | 35 +- .../Driver/DriverFactoryInterface.php | 23 + .../RemoteStorage/Driver/DriverPool.php | 72 + app/code/Magento/RemoteStorage/LICENSE.txt | 48 + .../Magento/RemoteStorage/LICENSE_AFL.txt | 48 + .../Magento/RemoteStorage/Model/Config.php | 42 + .../Model/Config/Source/FileStorage.php | 37 + .../RemoteStorage/Model/Filesystem.php | 55 + .../Magento/RemoteStorage/Plugin/Sitemap.php | 64 + app/code/Magento/RemoteStorage/README.md | 1 + .../Test/Unit/Model/ConfigTest.php | 43 + app/code/Magento/RemoteStorage/composer.json | 25 + .../RemoteStorage/etc/adminhtml/di.xml | 19 + .../RemoteStorage/etc/adminhtml/system.xml | 20 + app/code/Magento/RemoteStorage/etc/di.xml | 51 + app/code/Magento/RemoteStorage/etc/module.xml | 15 + .../Magento/RemoteStorage/registration.php | 11 + app/code/Magento/Sitemap/Block/Robots.php | 4 + app/code/Magento/Sitemap/Model/Sitemap.php | 9 +- app/etc/di.xml | 1 + composer.json | 8 +- composer.lock | 1300 ++++++++++------- .../Magento/Framework/Api/Uploader.php | 20 + .../Test/Unit/Config}/DocumentRootTest.php | 4 +- .../Magento/Framework/Config/DocumentRoot.php | 50 + .../Magento/Framework/File/Uploader.php | 77 +- .../Filesystem/Directory/ReadFactory.php | 10 +- .../Filesystem/Directory/TargetDirectory.php | 60 + .../Filesystem/Directory/WriteFactory.php | 10 +- .../Framework/Filesystem/Driver/File.php | 6 +- .../Framework/Filesystem/DriverPool.php | 2 +- .../Filesystem/DriverPoolInterface.php | 22 + .../Framework/Filesystem/File/ReadFactory.php | 8 +- .../Filesystem/File/WriteFactory.php | 5 +- .../Framework/Setup/FilePermissions.php | 9 +- .../Setup/Test/Unit/FilePermissionsTest.php | 38 +- 48 files changed, 2546 insertions(+), 596 deletions(-) create mode 100644 app/code/Magento/AwsS3/Driver/AwsS3.php create mode 100644 app/code/Magento/AwsS3/Driver/AwsS3Factory.php create mode 100644 app/code/Magento/AwsS3/LICENSE.txt create mode 100644 app/code/Magento/AwsS3/LICENSE_AFL.txt create mode 100644 app/code/Magento/AwsS3/Model/Config.php create mode 100644 app/code/Magento/AwsS3/README.md create mode 100644 app/code/Magento/AwsS3/composer.json create mode 100644 app/code/Magento/AwsS3/etc/adminhtml/di.xml create mode 100644 app/code/Magento/AwsS3/etc/adminhtml/system.xml create mode 100644 app/code/Magento/AwsS3/etc/di.xml create mode 100644 app/code/Magento/AwsS3/etc/module.xml create mode 100644 app/code/Magento/AwsS3/registration.php create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverPool.php create mode 100644 app/code/Magento/RemoteStorage/LICENSE.txt create mode 100644 app/code/Magento/RemoteStorage/LICENSE_AFL.txt create mode 100644 app/code/Magento/RemoteStorage/Model/Config.php create mode 100644 app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php create mode 100644 app/code/Magento/RemoteStorage/Model/Filesystem.php create mode 100644 app/code/Magento/RemoteStorage/Plugin/Sitemap.php create mode 100644 app/code/Magento/RemoteStorage/README.md create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php create mode 100644 app/code/Magento/RemoteStorage/composer.json create mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/di.xml create mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/system.xml create mode 100644 app/code/Magento/RemoteStorage/etc/di.xml create mode 100644 app/code/Magento/RemoteStorage/etc/module.xml create mode 100644 app/code/Magento/RemoteStorage/registration.php rename {app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed => lib/internal/Magento/Framework/App/Test/Unit/Config}/DocumentRootTest.php (92%) create mode 100644 lib/internal/Magento/Framework/Config/DocumentRoot.php create mode 100644 lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php create mode 100644 lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php new file mode 100644 index 0000000000000..602e81ec480ff --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -0,0 +1,553 @@ + $region, + 'version' => 'latest' + ]; + + if ($key && $secret) { + $config['credentials'] = [ + 'key' => $key, + 'secret' => $secret, + ]; + } + + $client = new S3Client($config); + $this->adapter = new AwsS3Adapter($client, $bucket); + } + + /** + * Destroy opened streams. + * + * @throws FileSystemException + */ + public function __destruct() + { + foreach ($this->streams as $stream) { + $this->fileClose($stream); + } + } + + /** + * @inheritDoc + */ + public function fileGetContents($path, $flag = null, $context = null): string + { + $path = $this->getRelativePath('', $path); + + if (isset($this->streams[$path])) { + //phpcs:disable + return file_get_contents(stream_get_meta_data($this->streams[$path])['uri']); + //phpcs:enable + } + + return $this->adapter->read($path)['contents']; + } + + /** + * @inheritDoc + */ + public function isExists($path): bool + { + if ($path === '/') { + return true; + } + + $path = $this->getRelativePath('', $path); + + if (!$path || $path === '/') { + return true; + } + + return $this->adapter->has($path); + } + + /** + * @inheritDoc + */ + public function isWritable($path): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function createDirectory($path, $permissions = 0777): bool + { + if ($path === '/') { + return true; + } + + $path = $this->getRelativePath('', $path); + + return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config([])); + } + + /** + * @inheritDoc + */ + public function copy($source, $destination, DriverInterface $targetDriver = null): bool + { + $source = $this->getRelativePath('', $source); + $destination = $this->getRelativePath('', $destination); + + return $this->adapter->copy($source, $destination); + } + + /** + * @inheritDoc + */ + public function deleteFile($path): bool + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->delete($path); + } + + /** + * @inheritDoc + */ + public function deleteDirectory($path): bool + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->deleteDir($path); + } + + /** + * @inheritDoc + */ + public function filePutContents($path, $content, $mode = null, $context = null): int + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->write($path, $content, new Config(['ACL' => 'public-read']))['size']; + } + + /** + * @inheritDoc + */ + public function readDirectoryRecursively($path = null): array + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->listContents($path, true); + } + + /** + * @inheritDoc + */ + public function readDirectory($path): array + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->listContents($path, false); + } + + /** + * @inheritDoc + */ + public function getRealPathSafety($path) + { + return '/'; + } + + /** + * @inheritDoc + */ + public function getAbsolutePath($basePath, $path, $scheme = null) + { + $path = $this->getRelativePath($basePath, $path); + + if ($path === '/') { + $path = ''; + } + + if ($basePath !== '/') { + $path = $basePath . $path; + } + + $path = $path ?: '.'; + + return $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), $path); + } + + /** + * @inheritDoc + */ + public function isReadable($path): bool + { + return $this->isExists($path); + } + + /** + * @inheritDoc + */ + public function isFile($path): bool + { + if ($path === '/') { + return false; + } + + $path = $this->getRelativePath('', $path); + $path = rtrim($path, '/'); + + return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_FILE; + } + + /** + * @inheritDoc + */ + public function isDirectory($path): bool + { + if ($path === '/') { + return true; + } + + $path = $this->getRelativePath('', $path); + + if (!$path || $path === '/') { + return true; + } + + $path = rtrim($path, '/') . '/'; + + return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_DIR; + } + + /** + * @inheritDoc + */ + public function getRelativePath($basePath, $path = null): string + { + $relativePath = str_replace( + $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), '.'), + '', + $path + ); + + if ($basePath && $basePath !== '/') { + $relativePath = str_replace($basePath, '', $relativePath); + } + + $relativePath = ltrim($relativePath, '/'); + + if (!$relativePath) { + $relativePath = '/'; + } + + return $relativePath; + } + + /** + * @inheritDoc + */ + public function getParentDirectory($path): string + { + return '/'; + } + + /** + * @inheritDoc + */ + public function getRealPath($path) + { + return $this->getAbsolutePath('', $path); + } + + /** + * @inheritDoc + */ + public function rename($oldPath, $newPath, DriverInterface $targetDriver = null): bool + { + $oldPath = $this->getRelativePath('', $oldPath); + $newPath = $this->getRelativePath('', $newPath); + + return $this->adapter->rename($oldPath, $newPath); + } + + /** + * @inheritDoc + */ + public function stat($path): array + { + $path = $this->getRelativePath('', $path); + $metaInfo = $this->adapter->getMetadata($path); + + if (!$metaInfo) { + throw new FileSystemException(__('Cannot gather stats! %1', (array)$path)); + } + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'atime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + 'size' => $metaInfo['size'], + 'type' => $metaInfo['type'], + 'mtime' => $metaInfo['timestamp'], + 'disposition' => null, + ]; + } + + /** + * @inheritDoc + */ + public function search($pattern, $path): array + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function symlink($source, $destination, DriverInterface $targetDriver = null): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function changePermissions($path, $permissions): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function changePermissionsRecursively($path, $dirPermissions, $filePermissions): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function touch($path, $modificationTime = null) + { + return true; + } + + /** + * @inheritDoc + */ + public function fileReadLine($resource, $length, $ending = null): string + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileRead($resource, $length): string + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = fread($resource, $length); + if ($result === false) { + throw new FileSystemException(__('File cannot be read %1', [$this->getWarningMessage()])); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure = '"', $escape = '\\') + { + //phpcs:disable + $metadata = stream_get_meta_data($resource); + //phpcs:enable + $file = $this->adapter->read($metadata['uri'])['contents']; + + return str_getcsv($file, $delimiter, $enclosure, $escape); + } + + /** + * @inheritDoc + */ + public function fileTell($resource): int + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileSeek($resource, $offset, $whence = SEEK_SET): int + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function endOfFile($resource): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function filePutCsv($resource, array $data, $delimiter = ',', $enclosure = '"') + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return fputcsv($resource, $data, $delimiter, $enclosure); + } + + /** + * @inheritDoc + */ + public function fileFlush($resource): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileLock($resource, $lockMode = LOCK_EX): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileUnlock($resource): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileWrite($resource, $data) + { + //phpcs:disable + $resourcePath = stream_get_meta_data($resource)['uri']; + + foreach ($this->streams as $stream) { + if (stream_get_meta_data($stream)['uri'] === $resourcePath) { + return fwrite($stream, $data); + } + } + //phpcs:enable + + return false; + } + + /** + * @inheritDoc + */ + public function fileClose($resource): bool + { + //phpcs:disable + $resourcePath = stream_get_meta_data($resource)['uri']; + + foreach ($this->streams as $path => $stream) { + if (stream_get_meta_data($stream)['uri'] === $resourcePath) { + $this->adapter->writeStream($path, $resource, new Config(['ACL' => 'public-read'])); + + // Remove path from streams after + unset($this->streams[$path]); + + return fclose($stream); + } + } + //phpcs:enable + + return false; + } + + /** + * @inheritDoc + */ + public function fileOpen($path, $mode) + { + $path = $this->getRelativePath('', $path); + + if (!isset($this->streams[$path])) { + $this->streams[$path] = tmpfile(); + if ($this->adapter->has($path)) { + $file = tmpfile(); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + fwrite($file, $this->adapter->read($path)['contents']); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + fseek($file, 0); + } else { + $file = tmpfile(); + } + $this->streams[$path] = $file; + } + + return $this->streams[$path]; + } + + /** + * Returns last warning message string + * + * @return string|null + */ + private function getWarningMessage(): ?string + { + $warning = error_get_last(); + if ($warning && $warning['type'] === E_WARNING) { + return 'Warning!' . $warning['message']; + } + + return null; + } +} diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php new file mode 100644 index 0000000000000..e71c3a84d3ce5 --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -0,0 +1,47 @@ +config = $config; + } + + /** + * Creates an instance of AWS S3 driver. + * + * @return DriverInterface + */ + public function create(): DriverInterface + { + return new AwsS3( + $this->config->getRegion(), + $this->config->getBucket(), + $this->config->getAccessKey(), + $this->config->getSecretKey() + ); + } +} diff --git a/app/code/Magento/AwsS3/LICENSE.txt b/app/code/Magento/AwsS3/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AwsS3/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 " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AwsS3/LICENSE_AFL.txt b/app/code/Magento/AwsS3/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AwsS3/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 " 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/AwsS3/Model/Config.php b/app/code/Magento/AwsS3/Model/Config.php new file mode 100644 index 0000000000000..00cd5b36740e6 --- /dev/null +++ b/app/code/Magento/AwsS3/Model/Config.php @@ -0,0 +1,75 @@ +config = $config; + } + + /** + * Retrieves region. + * + * @return string + */ + public function getRegion(): string + { + return (string)$this->config->getValue(self::PATH_REGION); + } + + /** + * Retrieves bucket. + * + * @return string + */ + public function getBucket(): string + { + return (string)$this->config->getValue(self::PATH_BUCKET); + } + + /** + * Retrieves access key. + * + * @return string + */ + public function getAccessKey(): string + { + return (string)$this->config->getValue(self::PATH_ACCESS_KEY); + } + + /** + * Retrieves secret key. + * + * @return string + */ + public function getSecretKey(): string + { + return (string)$this->config->getValue(self::PATH_SECRET_KEY); + } +} diff --git a/app/code/Magento/AwsS3/README.md b/app/code/Magento/AwsS3/README.md new file mode 100644 index 0000000000000..fc07df1717136 --- /dev/null +++ b/app/code/Magento/AwsS3/README.md @@ -0,0 +1,3 @@ +# Magento_AwsS3 module + +The Magento_AwsS3 module integrates your Magento with the [AWS S3](https://aws.amazon.com/s3) storage. diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json new file mode 100644 index 0000000000000..02733f01d2285 --- /dev/null +++ b/app/code/Magento/AwsS3/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-aws-s-3", + "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "^100.0.2", + "magento/module-remote-storage": "*", + "league/flysystem": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AwsS3\\": "" + } + } +} diff --git a/app/code/Magento/AwsS3/etc/adminhtml/di.xml b/app/code/Magento/AwsS3/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..4d3dcd601047f --- /dev/null +++ b/app/code/Magento/AwsS3/etc/adminhtml/di.xml @@ -0,0 +1,19 @@ + + + + + + + + aws-s3 + AWS S3 + + + + + diff --git a/app/code/Magento/AwsS3/etc/adminhtml/system.xml b/app/code/Magento/AwsS3/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..0f97b96107ed3 --- /dev/null +++ b/app/code/Magento/AwsS3/etc/adminhtml/system.xml @@ -0,0 +1,35 @@ + + + + +
+ + + + required-entry + aws-s3 + + + + required-entry + aws-s3 + + + + required-entry + aws-s3 + + + + required-entry + aws-s3 + + +
+
+
diff --git a/app/code/Magento/AwsS3/etc/di.xml b/app/code/Magento/AwsS3/etc/di.xml new file mode 100644 index 0000000000000..2b66da74299ea --- /dev/null +++ b/app/code/Magento/AwsS3/etc/di.xml @@ -0,0 +1,16 @@ + + + + + + + Magento\AwsS3\Driver\AwsS3Factory + + + + diff --git a/app/code/Magento/AwsS3/etc/module.xml b/app/code/Magento/AwsS3/etc/module.xml new file mode 100644 index 0000000000000..ab99195d45ab5 --- /dev/null +++ b/app/code/Magento/AwsS3/etc/module.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/AwsS3/registration.php b/app/code/Magento/AwsS3/registration.php new file mode 100644 index 0000000000000..496fbad1d3371 --- /dev/null +++ b/app/code/Magento/AwsS3/registration.php @@ -0,0 +1,9 @@ +config = $config; + $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(BaseDocumentRoot::class); } /** - * A shortcut to load the document root path from the DirectoryList based on the - * deployment configuration. + * A shortcut to load the document root path from the DirectoryList. * * @return string * @since 101.0.0 */ public function getPath() { - return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + return $this->documentRoot->getPath(); } /** - * Returns whether the deployment configuration specifies that the document root is - * in the pub/ folder. This affects ares such as sitemaps and robots.txt (and will - * likely be extended to control other areas). + * Checks if root folder is /pub. * * @return bool * @since 101.0.0 */ public function isPub() { - return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + return $this->documentRoot->isPub(); } } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php new file mode 100644 index 0000000000000..a95284fb27391 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -0,0 +1,23 @@ +driverPool = $driverPool; + $this->config = $config; + $this->remotePool = $remotePool; + } + + /** + * @inheritDoc + */ + public function getDriver($code = self::REMOTE): DriverInterface + { + $driver = $this->config->getValue('system/file_system/driver'); + + if (isset($this->pool[$code])) { + return $this->pool[$code]; + } + + if ($driver && $driver !== BaseDriverPool::FILE) { + return $this->pool[$code] = $this->remotePool[$driver]->create(); + } + + return $this->pool[$code] = $this->driverPool->getDriver($code); + } +} diff --git a/app/code/Magento/RemoteStorage/LICENSE.txt b/app/code/Magento/RemoteStorage/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/RemoteStorage/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 " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/RemoteStorage/LICENSE_AFL.txt b/app/code/Magento/RemoteStorage/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/RemoteStorage/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 " 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/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php new file mode 100644 index 0000000000000..b49c647ab6894 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -0,0 +1,42 @@ +scopeConfig = $scopeConfig; + } + + /** + * Check if remote FS is enabled. + * + * @return bool + */ + public function isEnabled(): bool + { + $driver = $this->scopeConfig->getValue('system/file_system/driver'); + + return $driver && $driver !== DriverPool::FILE; + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php b/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php new file mode 100644 index 0000000000000..4972cdda18d9b --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php @@ -0,0 +1,37 @@ +options = $options; + } + + /** + * @inheritDoc + */ + public function toOptionArray(): array + { + return $this->options; + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Filesystem.php b/app/code/Magento/RemoteStorage/Model/Filesystem.php new file mode 100644 index 0000000000000..040ee005cd57d --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Filesystem.php @@ -0,0 +1,55 @@ +isEnabled = $config->isEnabled(); + + parent::__construct($directoryList, $readFactory, $writeFactory); + } + + /** + * Gets URL path by code. + * + * @param string $code + * @return string + */ + protected function getDirPath($code): string + { + if ($this->isEnabled) { + return $this->directoryList->getUrlPath($code) ?: '/'; + } + + return parent::getDirPath($code); + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php new file mode 100644 index 0000000000000..e84f216ba996c --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php @@ -0,0 +1,64 @@ +driverPool = $driverPool; + $this->config = $config; + } + + /** + * Modifies image URl to point to correct remote storage. + * + * @param BaseSitemap $subject + * @param string $result + * @param string $sitemapPath + * @param string $sitemapFileName + * @return string + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetSitemapUrl( + BaseSitemap $subject, + string $result, + string $sitemapPath, + string $sitemapFileName + ): string { + if ($this->config->isEnabled()) { + $path = trim($sitemapPath . $sitemapFileName, '/'); + + return $this->driverPool->getDriver()->getAbsolutePath('', $path); + } + + return $result; + } +} diff --git a/app/code/Magento/RemoteStorage/README.md b/app/code/Magento/RemoteStorage/README.md new file mode 100644 index 0000000000000..f33b25795a995 --- /dev/null +++ b/app/code/Magento/RemoteStorage/README.md @@ -0,0 +1 @@ +# Magento_RemoteStorage module diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..1e121c1d1dda1 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,43 @@ +getMockForAbstractClass(ScopeConfigInterface::class); + $configMock->method('getValue') + ->willReturnMap([ + [DriverPool::PATH_DRIVER, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, DriverPool::REMOTE], + ]); + + $this->model = new Config($configMock); + } + + public function testIsEnabled(): void + { + self::assertTrue($this->model->isEnabled()); + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json new file mode 100644 index 0000000000000..105b2b2b21a46 --- /dev/null +++ b/app/code/Magento/RemoteStorage/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-remote-storage", + "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "^100.0.2" + }, + "suggest": { + "magento/module-backend": "*", + "magento/module-sitemap": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\RemoteStorage\\": "" + } + } +} diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..1437f0636ac03 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml @@ -0,0 +1,19 @@ + + + + + + + + Magento\Framework\Filesystem\DriverPool::FILE + File System + + + + + diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..aa5865c099dc5 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml @@ -0,0 +1,20 @@ + + + + +
+ + + + + Magento\RemoteStorage\Model\Config\Source\FileStorage + + +
+
+
diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml new file mode 100644 index 0000000000000..9bc960691d034 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -0,0 +1,51 @@ + + + + + + Magento\RemoteStorage\Driver\DriverPool + + + + + Magento\RemoteStorage\Driver\DriverPool + + + + + remoteWriteFactory + remoteReadFactory + + + + + remoteFilesystem + + + + + remoteFilesystem + + + + + remoteFilesystem + + + + + remoteFilesystem + + + + + + remoteFilesystem + + + diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml new file mode 100644 index 0000000000000..cc9f2e7328292 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/code/Magento/RemoteStorage/registration.php b/app/code/Magento/RemoteStorage/registration.php new file mode 100644 index 0000000000000..3a6d6b67a8dcf --- /dev/null +++ b/app/code/Magento/RemoteStorage/registration.php @@ -0,0 +1,11 @@ +addStoreFilter($storeIds); $sitemapLinks = []; + /** + * @var Sitemap $sitemap + */ foreach ($collection as $sitemap) { $sitemapUrl = $sitemap->getSitemapUrl($sitemap->getSitemapPath(), $sitemap->getSitemapFilename()); $sitemapLinks[$sitemapUrl] = 'Sitemap: ' . $sitemapUrl; diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 9a8d2c57a280c..ddb04f28d58d1 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -475,12 +475,9 @@ public function generateXml() if ($this->_sitemapIncrement == 1) { // In case when only one increment file was created use it as default sitemap - $path = rtrim( - $this->getSitemapPath(), - '/' - ) . '/' . $this->_getCurrentSitemapFilename( - $this->_sitemapIncrement - ); + $path = rtrim($this->getSitemapPath(), '/') + . '/' + . $this->_getCurrentSitemapFilename($this->_sitemapIncrement); $destination = rtrim($this->getSitemapPath(), '/') . '/' . $this->getSitemapFilename(); $this->_directory->renameFile($path, $destination); diff --git a/app/etc/di.xml b/app/etc/di.xml index 585c88f68ff6f..008671f2705e3 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -212,6 +212,7 @@ + diff --git a/composer.json b/composer.json index 57fbfaaa35c2b..985bf0d9e16ea 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,9 @@ "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", "webonyx/graphql-php": "^0.13.8", - "wikimedia/less.php": "~1.8.0" + "wikimedia/less.php": "~1.8.0", + "league/flysystem": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0" }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", @@ -323,7 +325,9 @@ "twbs/bootstrap": "3.1.0", "tinymce/tinymce": "3.4.7", "magento/module-tinymce-3": "*", - "magento/module-csp": "*" + "magento/module-csp": "*", + "magento/module-aws-s-3": "*", + "magento/module-remote-storage": "*" }, "conflict": { "gene/bluefoot": "*" diff --git a/composer.lock b/composer.lock index 8a5d82536cee4..6d5c895670800 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,93 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", + "content-hash": "3eb0d410285c05a9f2649b65d8b9a1d5", "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.154.7", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "fa2bf35b5d80e9597e5a2cd7e337eeeb44d09d9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/fa2bf35b5d80e9597e5a2cd7e337eeeb44d09d9c", + "reference": "fa2bf35b5d80e9597e5a2cd7e337eeeb44d09d9c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "^2.5", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^4.8.35|^5.4.3", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2020-09-21T18:12:58+00:00" + }, { "name": "colinmollenhour/cache-backend-file", "version": "v1.4.5", @@ -154,16 +239,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.7", + "version": "1.2.8", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd" + "reference": "8a7ecad675253e4654ea05505233285377405215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", + "reference": "8a7ecad675253e4654ea05505233285377405215", "shasum": "" }, "require": { @@ -211,25 +296,29 @@ "url": "https://packagist.com", "type": "custom" }, + { + "url": "https://github.com/composer", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2020-04-08T08:27:21+00:00" + "time": "2020-08-23T12:54:47+00:00" }, { "name": "composer/composer", - "version": "1.10.9", + "version": "1.10.13", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "83c3250093d5491600a822e176b107a945baf95a" + "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/83c3250093d5491600a822e176b107a945baf95a", - "reference": "83c3250093d5491600a822e176b107a945baf95a", + "url": "https://api.github.com/repos/composer/composer/zipball/47c841ba3b2d3fc0b4b13282cf029ea18b66d78b", + "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b", "shasum": "" }, "require": { @@ -310,20 +399,20 @@ "type": "tidelift" } ], - "time": "2020-07-16T10:57:00+00:00" + "time": "2020-09-09T09:46:34+00:00" }, { "name": "composer/semver", - "version": "1.5.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" + "reference": "114f819054a2ea7db03287f5efb757e2af6e4079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "url": "https://api.github.com/repos/composer/semver/zipball/114f819054a2ea7db03287f5efb757e2af6e4079", + "reference": "114f819054a2ea7db03287f5efb757e2af6e4079", "shasum": "" }, "require": { @@ -371,7 +460,21 @@ "validation", "versioning" ], - "time": "2020-01-13T12:06:48+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-09-09T09:34:06+00:00" }, { "name": "composer/spdx-licenses", @@ -449,16 +552,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", "shasum": "" }, "require": { @@ -503,7 +606,7 @@ "type": "tidelift" } ], - "time": "2020-06-04T11:16:35+00:00" + "time": "2020-08-19T10:27:58+00:00" }, { "name": "container-interop/container-interop", @@ -1356,6 +1459,12 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -1535,41 +1644,41 @@ }, { "name": "laminas/laminas-eventmanager", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-eventmanager.git", - "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748" + "reference": "1940ccf30e058b2fd66f5a9d696f1b5e0027b082" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", - "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", + "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/1940ccf30e058b2fd66f5a9d696f1b5e0027b082", + "reference": "1940ccf30e058b2fd66f5a9d696f1b5e0027b082", "shasum": "" }, "require": { "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ^8.0" }, "replace": { - "zendframework/zend-eventmanager": "self.version" + "zendframework/zend-eventmanager": "^3.2.1" }, "require-dev": { - "athletic/athletic": "^0.1", - "container-interop/container-interop": "^1.1.0", + "container-interop/container-interop": "^1.1", "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-stdlib": "^2.7.3 || ^3.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^8.5.8" }, "suggest": { - "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "container-interop/container-interop": "^1.1, to use the lazy listeners feature", "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev", - "dev-develop": "3.3-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -1589,20 +1698,26 @@ "events", "laminas" ], - "time": "2019-12-31T16:44:52+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-25T11:10:44+00:00" }, { "name": "laminas/laminas-feed", - "version": "2.12.2", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-feed.git", - "reference": "8a193ac96ebcb3e16b6ee754ac2a889eefacb654" + "reference": "3c91415633cb1be6f9d78683d69b7dcbfe6b4012" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/8a193ac96ebcb3e16b6ee754ac2a889eefacb654", - "reference": "8a193ac96ebcb3e16b6ee754ac2a889eefacb654", + "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/3c91415633cb1be6f9d78683d69b7dcbfe6b4012", + "reference": "3c91415633cb1be6f9d78683d69b7dcbfe6b4012", "shasum": "" }, "require": { @@ -1656,7 +1771,13 @@ "feed", "laminas" ], - "time": "2020-03-29T12:36:29+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-18T13:45:04+00:00" }, { "name": "laminas/laminas-filter", @@ -1817,16 +1938,16 @@ }, { "name": "laminas/laminas-http", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-http.git", - "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575" + "reference": "33b7942f51ce905ce9bfc8bf28badc501d3904b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/48bd06ffa3a6875e2b77d6852405eb7b1589d575", - "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/33b7942f51ce905ce9bfc8bf28badc501d3904b5", + "reference": "33b7942f51ce905ce9bfc8bf28badc501d3904b5", "shasum": "" }, "require": { @@ -1849,12 +1970,6 @@ "paragonie/certainty": "For automated management of cacert.pem" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.12.x-dev", - "dev-develop": "2.13.x-dev" - } - }, "autoload": { "psr-4": { "Laminas\\Http\\": "src/" @@ -1877,7 +1992,7 @@ "type": "community_bridge" } ], - "time": "2020-06-23T15:14:37+00:00" + "time": "2020-08-18T17:11:58+00:00" }, { "name": "laminas/laminas-hydrator", @@ -2263,16 +2378,16 @@ }, { "name": "laminas/laminas-mail", - "version": "2.11.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mail.git", - "reference": "4c5545637eea3dc745668ddff1028692ed004c4b" + "reference": "c154a733b122539ac2c894561996c770db289f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/4c5545637eea3dc745668ddff1028692ed004c4b", - "reference": "4c5545637eea3dc745668ddff1028692ed004c4b", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/c154a733b122539ac2c894561996c770db289f70", + "reference": "c154a733b122539ac2c894561996c770db289f70", "shasum": "" }, "require": { @@ -2282,7 +2397,7 @@ "laminas/laminas-stdlib": "^2.7 || ^3.0", "laminas/laminas-validator": "^2.10.2", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0", + "php": "^7.1", "true/punycode": "^2.1" }, "replace": { @@ -2292,8 +2407,8 @@ "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-config": "^2.6", "laminas/laminas-crypt": "^2.6 || ^3.0", - "laminas/laminas-servicemanager": "^2.7.10 || ^3.3.1", - "phpunit/phpunit": "^5.7.25 || ^6.4.4 || ^7.1.4" + "laminas/laminas-servicemanager": "^3.2.1", + "phpunit/phpunit": "^7.5.20" }, "suggest": { "laminas/laminas-crypt": "Crammd5 support in SMTP Auth", @@ -2301,10 +2416,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.11.x-dev", - "dev-develop": "2.12.x-dev" - }, "laminas": { "component": "Laminas\\Mail", "config-provider": "Laminas\\Mail\\ConfigProvider" @@ -2331,7 +2442,7 @@ "type": "community_bridge" } ], - "time": "2020-06-30T20:17:23+00:00" + "time": "2020-08-12T14:51:33+00:00" }, { "name": "laminas/laminas-math", @@ -2443,16 +2554,16 @@ }, { "name": "laminas/laminas-modulemanager", - "version": "2.8.4", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-modulemanager.git", - "reference": "92b1cde1aab5aef687b863face6dd5d9c6751c78" + "reference": "789bbd4ab391da9221f265f6bb2d594f8f11855b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/92b1cde1aab5aef687b863face6dd5d9c6751c78", - "reference": "92b1cde1aab5aef687b863face6dd5d9c6751c78", + "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/789bbd4ab391da9221f265f6bb2d594f8f11855b", + "reference": "789bbd4ab391da9221f265f6bb2d594f8f11855b", "shasum": "" }, "require": { @@ -2460,10 +2571,11 @@ "laminas/laminas-eventmanager": "^3.2 || ^2.6.3", "laminas/laminas-stdlib": "^3.1 || ^2.7", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0", + "webimpress/safe-writer": "^1.0.2 || ^2.1" }, "replace": { - "zendframework/zend-modulemanager": "self.version" + "zendframework/zend-modulemanager": "^2.8.4" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", @@ -2483,8 +2595,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" } }, "autoload": { @@ -2502,7 +2614,13 @@ "laminas", "modulemanager" ], - "time": "2019-12-31T17:26:56+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-25T09:29:22+00:00" }, { "name": "laminas/laminas-mvc", @@ -2952,35 +3070,35 @@ }, { "name": "laminas/laminas-stdlib", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-stdlib.git", - "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6" + "reference": "b9d84eaa39fde733356ea948cdef36c631f202b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/2b18347625a2f06a1a485acfbc870f699dbe51c6", - "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b9d84eaa39fde733356ea948cdef36c631f202b6", + "reference": "b9d84eaa39fde733356ea948cdef36c631f202b6", "shasum": "" }, "require": { "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ^8.0" }, "replace": { - "zendframework/zend-stdlib": "self.version" + "zendframework/zend-stdlib": "^3.2.1" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", - "phpbench/phpbench": "^0.13", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^9.3.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev", - "dev-develop": "3.3.x-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -2998,7 +3116,13 @@ "laminas", "stdlib" ], - "time": "2019-12-31T17:51:15+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-25T09:08:16+00:00" }, { "name": "laminas/laminas-text", @@ -3275,31 +3399,27 @@ }, { "name": "laminas/laminas-zendframework-bridge", - "version": "1.0.4", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-zendframework-bridge.git", - "reference": "fcd87520e4943d968557803919523772475e8ea3" + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/fcd87520e4943d968557803919523772475e8ea3", - "reference": "fcd87520e4943d968557803919523772475e8ea3", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev", - "dev-develop": "1.1.x-dev" - }, "laminas": { "module": "Laminas\\ZendFrameworkBridge" } @@ -3323,7 +3443,202 @@ "laminas", "zf" ], - "time": "2020-05-20T16:45:56+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-09-14T14:23:00+00:00" + }, + { + "name": "league/flysystem", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2020-08-23T07:39:11+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "1.0.28", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af7384a12f7cd7d08183390d930c9d0ec629c990", + "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.20.0", + "league/flysystem": "^1.0.40", + "php": ">=5.5.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3v3\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for the AWS S3 SDK v3.x", + "time": "2020-08-22T08:43:01+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ea2fbfc988bade315acd5967e6d02274086d0f28", + "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.36", + "phpunit/phpunit": "^8.5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2020-09-21T18:10:53+00:00" }, { "name": "magento/composer", @@ -3489,16 +3804,16 @@ }, { "name": "monolog/monolog", - "version": "1.25.4", + "version": "1.25.5", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "3022efff205e2448b560c833c6fbbf91c3139168" + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/3022efff205e2448b560c833c6fbbf91c3139168", - "reference": "3022efff205e2448b560c833c6fbbf91c3139168", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1817faadd1846cd08be9a49e905dc68823bc38c0", + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0", "shasum": "" }, "require": { @@ -3562,7 +3877,74 @@ "logging", "psr-3" ], - "time": "2020-05-22T07:31:27+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-07-23T08:35:51+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2020-07-31T21:01:56+00:00" }, { "name": "paragonie/random_compat", @@ -3889,16 +4271,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.28", + "version": "2.0.29", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260" + "reference": "497856a8d997f640b4a516062f84228a772a48a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", - "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/497856a8d997f640b4a516062f84228a772a48a8", + "reference": "497856a8d997f640b4a516062f84228a772a48a8", "shasum": "" }, "require": { @@ -3907,7 +4289,6 @@ "require-dev": { "phing/phing": "~2.7", "phpunit/phpunit": "^4.8.35|^5.7|^6.0", - "sami/sami": "~2.0", "squizlabs/php_codesniffer": "~2.0" }, "suggest": { @@ -3991,7 +4372,7 @@ "type": "tidelift" } ], - "time": "2020-07-08T09:08:33+00:00" + "time": "2020-09-08T04:24:43+00:00" }, { "name": "psr/container", @@ -4309,16 +4690,16 @@ }, { "name": "seld/jsonlint", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1" + "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/590cfec960b77fd55e39b7d9246659e95dd6d337", + "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337", "shasum": "" }, "require": { @@ -4354,7 +4735,17 @@ "parser", "validator" ], - "time": "2020-04-30T19:05:18+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2020-08-25T06:56:57+00:00" }, { "name": "seld/phar-utils", @@ -4402,16 +4793,16 @@ }, { "name": "symfony/console", - "version": "v4.4.10", + "version": "v4.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "326b064d804043005526f5a0494cfb49edb59bb0" + "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/326b064d804043005526f5a0494cfb49edb59bb0", - "reference": "326b064d804043005526f5a0494cfb49edb59bb0", + "url": "https://api.github.com/repos/symfony/console/zipball/b39fd99b9297b67fb7633b7d8083957a97e1e727", + "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727", "shasum": "" }, "require": { @@ -4489,11 +4880,11 @@ "type": "tidelift" } ], - "time": "2020-05-30T20:06:45+00:00" + "time": "2020-09-02T07:07:21+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -4560,16 +4951,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.4.10", + "version": "v4.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866" + "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5370aaa7807c7a439b21386661ffccf3dff2866", - "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e8ea5ccddd00556b86d69d42f99f1061a704030", + "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030", "shasum": "" }, "require": { @@ -4640,7 +5031,7 @@ "type": "tidelift" } ], - "time": "2020-05-20T08:37:50+00:00" + "time": "2020-08-13T14:18:44+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4720,16 +5111,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6e4320f06d5f2cce0d96530162491f4465179157" + "reference": "f7b9ed6142a34252d219801d9767dedbd711da1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", - "reference": "6e4320f06d5f2cce0d96530162491f4465179157", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/f7b9ed6142a34252d219801d9767dedbd711da1a", + "reference": "f7b9ed6142a34252d219801d9767dedbd711da1a", "shasum": "" }, "require": { @@ -4780,20 +5171,20 @@ "type": "tidelift" } ], - "time": "2020-05-30T20:35:19+00:00" + "time": "2020-08-21T17:19:47+00:00" }, { "name": "symfony/finder", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" + "reference": "2b765f0cf6612b3636e738c0689b29aa63088d5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", - "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", + "url": "https://api.github.com/repos/symfony/finder/zipball/2b765f0cf6612b3636e738c0689b29aa63088d5d", + "reference": "2b765f0cf6612b3636e738c0689b29aa63088d5d", "shasum": "" }, "require": { @@ -4843,11 +5234,11 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-08-17T10:01:29+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4923,16 +5314,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe" + "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251", + "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251", "shasum": "" }, "require": { @@ -5004,11 +5395,11 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-08-04T06:02:08+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5089,7 +5480,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -5166,7 +5557,7 @@ }, { "name": "symfony/polyfill-php70", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", @@ -5243,7 +5634,7 @@ }, { "name": "symfony/polyfill-php72", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", @@ -5316,7 +5707,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -5392,7 +5783,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -5472,20 +5863,20 @@ }, { "name": "symfony/process", - "version": "v4.4.10", + "version": "v4.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5" + "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5", - "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479", + "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "type": "library", "extra": { @@ -5531,20 +5922,20 @@ "type": "tidelift" } ], - "time": "2020-05-30T20:06:45+00:00" + "time": "2020-07-23T08:31:43+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", - "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", "shasum": "" }, "require": { @@ -5557,7 +5948,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -5607,7 +5998,7 @@ "type": "tidelift" } ], - "time": "2020-07-06T13:23:11+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "tedivm/jshrink", @@ -5754,6 +6145,61 @@ ], "time": "2018-01-15T15:26:51+00:00" }, + { + "name": "webimpress/safe-writer", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "5cfafdec5873c389036f14bf832a5efc9390dcdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/5cfafdec5873c389036f14bf832a5efc9390dcdd", + "reference": "5cfafdec5873c389036f14bf832a5efc9390dcdd", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.8 || ^9.3.7", + "vimeo/psalm": "^3.14.2", + "webimpress/coding-standard": "^1.1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2020-08-25T07:21:11+00:00" + }, { "name": "webonyx/graphql-php", "version": "v0.13.9", @@ -5877,16 +6323,16 @@ "packages-dev": [ { "name": "allure-framework/allure-codeception", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "9e0e25f8960fa5ac17c65c932ea8153ce6700713" + "reference": "a69800eeef83007ced9502a3349ff72f5fb6b4e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9e0e25f8960fa5ac17c65c932ea8153ce6700713", - "reference": "9e0e25f8960fa5ac17c65c932ea8153ce6700713", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/a69800eeef83007ced9502a3349ff72f5fb6b4e2", + "reference": "a69800eeef83007ced9502a3349ff72f5fb6b4e2", "shasum": "" }, "require": { @@ -5924,7 +6370,7 @@ "steps", "testing" ], - "time": "2020-03-13T11:07:13+00:00" + "time": "2020-09-09T10:51:33+00:00" }, { "name": "allure-framework/allure-php-api", @@ -5984,111 +6430,26 @@ "version": "1.2.4", "source": { "type": "git", - "url": "https://github.com/allure-framework/allure-phpunit.git", - "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/9399629c6eed79da4be18fd22adf83ef36c2d2e0", - "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0", - "shasum": "" - }, - "require": { - "allure-framework/allure-php-api": "~1.1.0", - "mikey179/vfsstream": "1.*", - "php": ">=7.1.0", - "phpunit/phpunit": ">=7.0.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Yandex": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Ivan Krutov", - "email": "vania-pooh@yandex-team.ru", - "role": "Developer" - } - ], - "description": "A PHPUnit adapter for Allure report.", - "homepage": "http://allure.qatools.ru/", - "keywords": [ - "allure", - "attachments", - "cases", - "phpunit", - "report", - "steps", - "testing" - ], - "time": "2018-10-25T12:03:54+00:00" - }, - { - "name": "aws/aws-sdk-php", - "version": "3.147.1", - "source": { - "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc" + "url": "https://github.com/allure-framework/allure-phpunit.git", + "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8a561a4a1645ccdd06413a4f2defe55d35e0eecc", - "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc", + "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/9399629c6eed79da4be18fd22adf83ef36c2d2e0", + "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0", "shasum": "" }, "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4.1", - "mtdowling/jmespath.php": "^2.5", - "php": ">=5.5" - }, - "require-dev": { - "andrewsville/php-token-reflection": "^1.4", - "aws/aws-php-sns-message-validator": "~1.0", - "behat/behat": "~3.0", - "doctrine/cache": "~1.4", - "ext-dom": "*", - "ext-openssl": "*", - "ext-pcntl": "*", - "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35|^5.4.3", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" - }, - "suggest": { - "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", - "doctrine/cache": "To use the DoctrineCacheAdapter", - "ext-curl": "To send requests using cURL", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", - "ext-sockets": "To use client-side monitoring" + "allure-framework/allure-php-api": "~1.1.0", + "mikey179/vfsstream": "1.*", + "php": ">=7.1.0", + "phpunit/phpunit": ">=7.0.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, "autoload": { - "psr-4": { - "Aws\\": "src/" - }, - "files": [ - "src/functions.php" - ] + "psr-0": { + "Yandex": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6096,23 +6457,23 @@ ], "authors": [ { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" + "name": "Ivan Krutov", + "email": "vania-pooh@yandex-team.ru", + "role": "Developer" } ], - "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", + "description": "A PHPUnit adapter for Allure report.", + "homepage": "http://allure.qatools.ru/", "keywords": [ - "amazon", - "aws", - "cloud", - "dynamodb", - "ec2", - "glacier", - "s3", - "sdk" + "allure", + "attachments", + "cases", + "phpunit", + "report", + "steps", + "testing" ], - "time": "2020-07-20T18:18:31+00:00" + "time": "2018-10-25T12:03:54+00:00" }, { "name": "beberlei/assert", @@ -6330,16 +6691,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.6", + "version": "4.1.7", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9" + "reference": "220ad18d3c192137d9dc2d0dd8d69a0d82083a26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", - "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/220ad18d3c192137d9dc2d0dd8d69a0d82083a26", + "reference": "220ad18d3c192137d9dc2d0dd8d69a0d82083a26", "shasum": "" }, "require": { @@ -6417,24 +6778,25 @@ "type": "open_collective" } ], - "time": "2020-06-07T16:31:51+00:00" + "time": "2020-08-28T06:37:06+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.12.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "acd0dc8b394595a74b58dcc889f72569ff7d8e71" + "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/acd0dc8b394595a74b58dcc889f72569ff7d8e71", - "reference": "acd0dc8b394595a74b58dcc889f72569ff7d8e71", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/263ef0b7eff80643e82f4cf55351eca553a09a10", + "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10", "shasum": "" }, "require": { "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", + "ext-dom": "*", "php": ">=5.6.0 <8.0" }, "type": "library", @@ -6455,32 +6817,36 @@ }, { "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], "description": "Assertion methods used by Codeception core and Asserts module", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "codeception" ], - "time": "2020-04-17T18:20:46+00:00" + "time": "2020-08-28T07:49:36+00:00" }, { "name": "codeception/module-asserts", - "version": "1.2.1", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "79f13d05b63f2fceba4d0e78044bab668c9b2a6b" + "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/79f13d05b63f2fceba4d0e78044bab668c9b2a6b", - "reference": "79f13d05b63f2fceba4d0e78044bab668c9b2a6b", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/32e5be519faaeb60ed3692383dcd1b3390ec2667", + "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667", "shasum": "" }, "require": { "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^1.12.0", + "codeception/lib-asserts": "^1.13.1", "php": ">=5.6.0 <8.0" }, "conflict": { @@ -6505,16 +6871,20 @@ }, { "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], "description": "Codeception module containing various assertions", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "assertions", "asserts", "codeception" ], - "time": "2020-04-20T07:26:11+00:00" + "time": "2020-08-28T08:06:29+00:00" }, { "name": "codeception/module-sequence", @@ -6561,16 +6931,16 @@ }, { "name": "codeception/module-webdriver", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "09c167817393090ce3dbce96027d94656b1963ce" + "reference": "237c6cb42d3e914f011d0419e966cbe0cb5d82c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/09c167817393090ce3dbce96027d94656b1963ce", - "reference": "09c167817393090ce3dbce96027d94656b1963ce", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/237c6cb42d3e914f011d0419e966cbe0cb5d82c6", + "reference": "237c6cb42d3e914f011d0419e966cbe0cb5d82c6", "shasum": "" }, "require": { @@ -6612,20 +6982,20 @@ "browser-testing", "codeception" ], - "time": "2020-05-31T08:47:24+00:00" + "time": "2020-08-06T07:39:31+00:00" }, { "name": "codeception/phpunit-wrapper", - "version": "9.0.2", + "version": "9.0.4", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "eb27243d8edde68593bf8d9ef5e9074734777931" + "reference": "bb0925f1fe7a30105208352e619a11d6096e7047" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/eb27243d8edde68593bf8d9ef5e9074734777931", - "reference": "eb27243d8edde68593bf8d9ef5e9074734777931", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/bb0925f1fe7a30105208352e619a11d6096e7047", + "reference": "bb0925f1fe7a30105208352e619a11d6096e7047", "shasum": "" }, "require": { @@ -6656,7 +7026,7 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2020-04-17T18:16:31+00:00" + "time": "2020-08-26T18:15:09+00:00" }, { "name": "codeception/stub", @@ -6842,16 +7212,16 @@ }, { "name": "doctrine/annotations", - "version": "1.10.3", + "version": "1.10.4", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" + "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", - "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", + "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", "shasum": "" }, "require": { @@ -6861,7 +7231,8 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^7.5" + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", "extra": { @@ -6907,7 +7278,7 @@ "docblock", "parser" ], - "time": "2020-05-25T17:24:27+00:00" + "time": "2020-08-10T19:35:50+00:00" }, { "name": "doctrine/cache", @@ -8038,90 +8409,6 @@ ], "time": "2020-02-22T20:59:37+00:00" }, - { - "name": "league/flysystem", - "version": "1.0.69", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "7106f78428a344bc4f643c233a94e48795f10967" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7106f78428a344bc4f643c233a94e48795f10967", - "reference": "7106f78428a344bc4f643c233a94e48795f10967", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": ">=5.5.9" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" - }, - "require-dev": { - "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7.26" - }, - "suggest": { - "ext-fileinfo": "Required for MimeType", - "ext-ftp": "Allows you to use FTP server storage", - "ext-openssl": "Allows you to use FTPS server storage", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Flysystem\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net" - } - ], - "description": "Filesystem abstraction: Many filesystems, one API.", - "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" - ], - "time": "2020-05-18T15:13:39+00:00" - }, { "name": "lusitanian/oauth", "version": "v0.8.11", @@ -8365,63 +8652,6 @@ "homepage": "http://vfs.bovigo.org/", "time": "2019-10-30T15:31:00+00:00" }, - { - "name": "mtdowling/jmespath.php", - "version": "2.5.0", - "source": { - "type": "git", - "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "52168cb9472de06979613d365c7f1ab8798be895" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/52168cb9472de06979613d365c7f1ab8798be895", - "reference": "52168cb9472de06979613d365c7f1ab8798be895", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "symfony/polyfill-mbstring": "^1.4" - }, - "require-dev": { - "composer/xdebug-handler": "^1.2", - "phpunit/phpunit": "^4.8.36|^7.5.15" - }, - "bin": [ - "bin/jp.php" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-4": { - "JmesPath\\": "src/" - }, - "files": [ - "src/JmesPath.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Declaratively specify how to extract elements from a JSON document", - "keywords": [ - "json", - "jsonpath" - ], - "time": "2019-12-30T18:03:34+00:00" - }, { "name": "mustache/mustache", "version": "v2.13.0", @@ -9006,16 +9236,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.0", + "version": "5.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df" + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { @@ -9054,20 +9284,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-07-20T20:05:34+00:00" + "time": "2020-09-03T19:13:55+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { @@ -9099,20 +9329,20 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpmd/phpmd", - "version": "2.8.2", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "714629ed782537f638fe23c4346637659b779a77" + "reference": "2a346575a45a6f00e631f4d7f3f71b6a05e0d46d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/714629ed782537f638fe23c4346637659b779a77", - "reference": "714629ed782537f638fe23c4346637659b779a77", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/2a346575a45a6f00e631f4d7f3f71b6a05e0d46d", + "reference": "2a346575a45a6f00e631f4d7f3f71b6a05e0d46d", "shasum": "" }, "require": { @@ -9123,6 +9353,8 @@ }, "require-dev": { "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", "gregwar/rst": "^1.0", "mikey179/vfsstream": "^1.6.4", "phpunit/phpunit": "^4.8.36 || ^5.7.27", @@ -9169,7 +9401,13 @@ "phpmd", "pmd" ], - "time": "2020-02-16T20:15:50+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2020-09-02T09:12:27+00:00" }, { "name": "phpoption/phpoption", @@ -9339,6 +9577,20 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], "time": "2020-05-05T12:55:44+00:00" }, { @@ -9469,16 +9721,16 @@ }, { "name": "phpunit/php-invoker", - "version": "3.0.2", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", - "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7a85b66acc48cacffdf87dadd3694e7123674298", + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298", "shasum": "" }, "require": { @@ -9494,7 +9746,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -9524,7 +9776,7 @@ "type": "github" } ], - "time": "2020-06-26T11:53:53+00:00" + "time": "2020-08-06T07:04:15+00:00" }, { "name": "phpunit/php-text-template", @@ -9628,20 +9880,26 @@ "keywords": [ "timer" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-04-20T06:00:37+00:00" }, { "name": "phpunit/php-token-stream", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", - "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", "shasum": "" }, "require": { @@ -9684,7 +9942,7 @@ } ], "abandoned": true, - "time": "2020-06-27T06:36:25+00:00" + "time": "2020-08-04T08:28:15+00:00" }, { "name": "phpunit/phpunit", @@ -9772,6 +10030,16 @@ "testing", "xunit" ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-05-22T13:54:05+00:00" }, { @@ -10775,16 +11043,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.5", + "version": "3.5.6", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", "shasum": "" }, "require": { @@ -10822,20 +11090,20 @@ "phpcs", "standards" ], - "time": "2020-04-17T01:09:41+00:00" + "time": "2020-08-10T04:50:15+00:00" }, { "name": "symfony/config", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "b8623ef3d99fe62a34baf7a111b576216965f880" + "reference": "22f961ddffdc81389670b2ca74a1cc0213761ec0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/b8623ef3d99fe62a34baf7a111b576216965f880", - "reference": "b8623ef3d99fe62a34baf7a111b576216965f880", + "url": "https://api.github.com/repos/symfony/config/zipball/22f961ddffdc81389670b2ca74a1cc0213761ec0", + "reference": "22f961ddffdc81389670b2ca74a1cc0213761ec0", "shasum": "" }, "require": { @@ -10902,20 +11170,20 @@ "type": "tidelift" } ], - "time": "2020-05-23T13:08:13+00:00" + "time": "2020-08-17T07:48:54+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "6508423eded583fc07e88a0172803e1a62f0310c" + "reference": "48d6890e12ce9cd8e68aaa4fb72010139312fd73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6508423eded583fc07e88a0172803e1a62f0310c", - "reference": "6508423eded583fc07e88a0172803e1a62f0310c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/48d6890e12ce9cd8e68aaa4fb72010139312fd73", + "reference": "48d6890e12ce9cd8e68aaa4fb72010139312fd73", "shasum": "" }, "require": { @@ -10991,20 +11259,20 @@ "type": "tidelift" } ], - "time": "2020-06-12T08:11:32+00:00" + "time": "2020-09-01T18:07:16+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", "shasum": "" }, "require": { @@ -11013,7 +11281,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -11055,20 +11323,20 @@ "type": "tidelift" } ], - "time": "2020-06-06T08:49:21+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f93055171b847915225bd5b0a5792888419d8d75" + "reference": "41a4647f12870e9d41d9a7d72ff0614a27208558" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", - "reference": "f93055171b847915225bd5b0a5792888419d8d75", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/41a4647f12870e9d41d9a7d72ff0614a27208558", + "reference": "41a4647f12870e9d41d9a7d72ff0614a27208558", "shasum": "" }, "require": { @@ -11130,20 +11398,20 @@ "type": "tidelift" } ], - "time": "2020-06-15T06:52:54+00:00" + "time": "2020-08-17T07:48:54+00:00" }, { "name": "symfony/mime", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "c0c418f05e727606e85b482a8591519c4712cf45" + "reference": "89a2c9b4cb7b5aa516cf55f5194c384f444c81dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/c0c418f05e727606e85b482a8591519c4712cf45", - "reference": "c0c418f05e727606e85b482a8591519c4712cf45", + "url": "https://api.github.com/repos/symfony/mime/zipball/89a2c9b4cb7b5aa516cf55f5194c384f444c81dc", + "reference": "89a2c9b4cb7b5aa516cf55f5194c384f444c81dc", "shasum": "" }, "require": { @@ -11207,20 +11475,20 @@ "type": "tidelift" } ], - "time": "2020-06-09T15:07:35+00:00" + "time": "2020-08-17T10:01:29+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" + "reference": "9ff59517938f88d90b6e65311fef08faa640f681" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", - "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/9ff59517938f88d90b6e65311fef08faa640f681", + "reference": "9ff59517938f88d90b6e65311fef08faa640f681", "shasum": "" }, "require": { @@ -11277,11 +11545,11 @@ "type": "tidelift" } ], - "time": "2020-05-23T13:08:13+00:00" + "time": "2020-07-12T12:58:00+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -11345,16 +11613,16 @@ }, { "name": "symfony/yaml", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23" + "reference": "a44bd3a91bfbf8db12367fa6ffac9c3eb1a8804a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ea342353a3ef4f453809acc4ebc55382231d4d23", - "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a44bd3a91bfbf8db12367fa6ffac9c3eb1a8804a", + "reference": "a44bd3a91bfbf8db12367fa6ffac9c3eb1a8804a", "shasum": "" }, "require": { @@ -11418,20 +11686,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-08-26T08:30:57+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.1.3", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb" + "reference": "53e6692d8ad1a7d72078093ced170c218a2e8b79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/9f277171e296a3c8629c04ac93ec95ff0f208ccb", - "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/53e6692d8ad1a7d72078093ced170c218a2e8b79", + "reference": "53e6692d8ad1a7d72078093ced170c218a2e8b79", "shasum": "" }, "require": { @@ -11551,7 +11819,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-07-10T09:34:29+00:00" + "time": "2020-09-03T14:09:13+00:00" }, { "name": "theseer/fdomdocument", diff --git a/lib/internal/Magento/Framework/Api/Uploader.php b/lib/internal/Magento/Framework/Api/Uploader.php index 5cea3a34569a9..3a4019b9caf84 100644 --- a/lib/internal/Magento/Framework/Api/Uploader.php +++ b/lib/internal/Magento/Framework/Api/Uploader.php @@ -13,6 +13,8 @@ class Uploader extends \Magento\Framework\File\Uploader { /** * Avoid running the default constructor specific to FILE upload + * + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ public function __construct() { @@ -30,9 +32,27 @@ public function processFileAttributes($fileAttributes) $this->_file = $fileAttributes; if (!file_exists($this->_file['tmp_name'])) { $code = empty($this->_file['tmp_name']) ? self::TMP_NAME_EMPTY : 0; + + // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow throw new \Exception('File was not processed correctly.', $code); } else { $this->_fileExists = true; } } + + /** + * Move files from TMP folder into destination folder + * + * @param string $tmpPath + * @param string $destPath + * @return bool|void + */ + protected function _moveFile($tmpPath, $destPath) + { + if (is_uploaded_file($tmpPath)) { + return move_uploaded_file($tmpPath, $destPath); + } elseif (is_file($tmpPath)) { + return rename($tmpPath, $destPath); + } + } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php similarity index 92% rename from app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php rename to lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php index 6f1758f3d2b92..90c32b54f17c5 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php @@ -5,13 +5,13 @@ */ declare(strict_types=1); -namespace Magento\Config\Test\Unit\Model\Config\Reader\Source\Deployed; +namespace Magento\Framework\App\Test\Unit\Config; -use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; use Magento\Framework\App\Config; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Config\DocumentRoot; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php new file mode 100644 index 0000000000000..45ccc34f0ce5b --- /dev/null +++ b/lib/internal/Magento/Framework/Config/DocumentRoot.php @@ -0,0 +1,50 @@ +config = $config; + } + + /** + * A shortcut to load the document root path from the DirectoryList. + * + * @return string + */ + public function getPath(): string + { + return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + } + + /** + * Checks if root folder is /pub. + * + * @return bool + */ + public function isPub(): bool + { + return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + } +} diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 7ec5843ddcf18..d8c2ecf8cf99d 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -7,7 +7,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; use Magento\Framework\Validation\ValidationException; @@ -19,6 +21,7 @@ * validation by protected file extension list to extended class * * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api * @since 100.0.2 @@ -180,6 +183,16 @@ class Uploader */ private $fileDriver; + /** + * @var TargetDirectory + */ + private $targetDirectory; + + /** + * @var DocumentRoot + */ + private $documentRoot; + /** * Init upload * @@ -187,13 +200,17 @@ class Uploader * @param \Magento\Framework\File\Mime|null $fileMime * @param DirectoryList|null $directoryList * @param DriverPool|null $driverPool + * @param TargetDirectory|null $targetDirectory + * @param DocumentRoot|null $documentRoot * @throws \DomainException */ public function __construct( $fileId, Mime $fileMime = null, DirectoryList $directoryList = null, - DriverPool $driverPool = null + DriverPool $driverPool = null, + TargetDirectory $targetDirectory = null, + DocumentRoot $documentRoot = null ) { $this->directoryList= $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); @@ -205,7 +222,9 @@ public function __construct( $this->_fileExists = true; } $this->fileMime = $fileMime ?: ObjectManager::getInstance()->get(Mime::class); - $this->driverPool = $driverPool; + $this->driverPool = $driverPool ?: ObjectManager::getInstance()->get(DriverPool::class); + $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); + $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); } /** @@ -319,11 +338,57 @@ protected function chmod($file) */ protected function _moveFile($tmpPath, $destPath) { - if (is_uploaded_file($tmpPath)) { - return move_uploaded_file($tmpPath, $destPath); - } elseif (is_file($tmpPath)) { - return rename($tmpPath, $destPath); + $rootPath = $this->getDocumentRoot()->getPath(); + $destPath = str_replace($this->getDirectoryList()->getPath($rootPath), '', $destPath); + $directory = $this->getTargetDirectory()->getDirectoryWrite($rootPath); + + return $this->getFileDriver()->rename( + $tmpPath, + $directory->getAbsolutePath($destPath), + $directory->getDriver() + ); + } + + /** + * Retrieves target directory. + * + * @return TargetDirectory + */ + private function getTargetDirectory(): TargetDirectory + { + if (!isset($this->targetDirectory)) { + $this->targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); } + + return $this->targetDirectory; + } + + /** + * Retrieves document root. + * + * @return DocumentRoot + */ + private function getDocumentRoot(): DocumentRoot + { + if (!isset($this->documentRoot)) { + $this->documentRoot = ObjectManager::getInstance()->get(DocumentRoot::class); + } + + return $this->documentRoot; + } + + /** + * Retrieves directory list. + * + * @return DirectoryList + */ + private function getDirectoryList(): DirectoryList + { + if (!isset($this->directoryList)) { + $this->directoryList = ObjectManager::getInstance()->get(DirectoryList::class); + } + + return $this->directoryList; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php index 25a290455dc46..a3364d3be1c8c 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php @@ -6,22 +6,26 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; +/** + * The factory of the filesystem directory instances for read operations. + */ class ReadFactory { /** * Pool of filesystem drivers * - * @var DriverPool + * @var DriverPoolInterface */ private $driverPool; /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php b/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php new file mode 100644 index 0000000000000..836eb680c24f7 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php @@ -0,0 +1,60 @@ +filesystem = $filesystem; + $this->driverCode = $driverCode; + } + + /** + * Create an instance of directory with write permissions. + * + * @param string $directoryCode + * @return WriteInterface + * @throws FileSystemException + */ + public function getDirectoryWrite(string $directoryCode): WriteInterface + { + return $this->filesystem->getDirectoryWrite($directoryCode, $this->driverCode); + } + + /** + * Create an instance of directory with read permissions. + * + * @param string $directoryCode + * @return ReadInterface + */ + public function getDirectoryRead(string $directoryCode): ReadInterface + { + return $this->filesystem->getDirectoryRead($directoryCode, $this->driverCode); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php index ff14b12f62047..6f6bfe558176d 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php @@ -6,22 +6,26 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; +/** + * The factory of the filesystem directory instances for write operations. + */ class WriteFactory { /** * Pool of filesystem drivers * - * @var DriverPool + * @var DriverPoolInterface */ private $driverPool; /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 1affad5521372..1fdde276e4e51 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -257,7 +257,7 @@ public function readDirectory($path) $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - + $iterator = new \FilesystemIterator($path, $flags); $result = []; /** @var \FilesystemIterator $file */ @@ -305,7 +305,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) } else { $content = $this->fileGetContents($oldPath); if (false !== $targetDriver->filePutContents($newPath, $content)) { - $result = $this->deleteFile($newPath); + $result = $this->deleteFile($oldPath); } } if (!$result) { @@ -952,7 +952,7 @@ public function readDirectoryRecursively($path = null) $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - + try { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path, $flags), diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPool.php b/lib/internal/Magento/Framework/Filesystem/DriverPool.php index 435e51c26b012..dfd1e2abce01a 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverPool.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverPool.php @@ -9,7 +9,7 @@ /** * A pool of stream wrappers */ -class DriverPool +class DriverPool implements DriverPoolInterface { /**#@+ * Available driver types diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php new file mode 100644 index 0000000000000..b88db7518ba17 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php @@ -0,0 +1,22 @@ +driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index 7a9596586f56a..8bc62cb6573c4 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -7,6 +7,7 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; /** * Opens a file for reading and/or writing @@ -25,9 +26,9 @@ class WriteFactory extends ReadFactory /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { parent::__construct($driverPool); $this->driverPool = $driverPool; diff --git a/lib/internal/Magento/Framework/Setup/FilePermissions.php b/lib/internal/Magento/Framework/Setup/FilePermissions.php index af0db6498144e..8003f2241f22a 100644 --- a/lib/internal/Magento/Framework/Setup/FilePermissions.php +++ b/lib/internal/Magento/Framework/Setup/FilePermissions.php @@ -93,12 +93,16 @@ public function getInstallationWritableDirectories() $data = [ DirectoryList::CONFIG, DirectoryList::VAR_DIR, - DirectoryList::MEDIA, - DirectoryList::STATIC_VIEW, + DirectoryList::MEDIA ]; if ($this->state->getMode() !== State::MODE_PRODUCTION) { $data[] = DirectoryList::GENERATED; + /** + * Static files may be pre-generated on separate machine. + */ + $data[] = DirectoryList::STATIC_VIEW; } + foreach ($data as $code) { $this->installationWritableDirectories[$code] = $this->directoryList->getPath($code); } @@ -260,6 +264,7 @@ public function getMissingWritablePathsForInstallation($associative = false) if ($associative) { $missingPaths[$missingPath] = $this->nonWritablePathsInDirectories[$missingPath]; } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $missingPaths = array_merge( $missingPaths, $this->nonWritablePathsInDirectories[$missingPath] diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php index e3428c411130c..6e2b83887561d 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php @@ -76,8 +76,8 @@ public function testGetInstallationWritableDirectories($mageMode) BP . '/app/etc', BP . '/var', BP . '/pub/media', + BP . '/generated', BP . '/pub/static', - BP . '/generated' ]; $this->assertEquals($expected, $this->filePermissions->getInstallationWritableDirectories()); @@ -94,7 +94,6 @@ public function testGetInstallationWritableDirectoriesInProduction() BP . '/app/etc', BP . '/var', BP . '/pub/media', - BP . '/pub/static' ]; $this->assertEquals($expected, $this->filePermissions->getInstallationWritableDirectories()); @@ -188,8 +187,8 @@ public function testGetMissingWritableDirectoriesAndPathsForInstallation($mageMo $expected = [ BP . '/var', BP . '/pub/media', + BP . '/generated', BP . '/pub/static', - BP . '/generated' ]; $this->assertEquals( @@ -213,8 +212,7 @@ public function testGetMissingWritableDirectoriesAndPathsForInstallationInProduc $expected = [ BP . '/var', - BP . '/pub/media', - BP . '/pub/static' + BP . '/pub/media' ]; $this->assertEquals( @@ -283,10 +281,15 @@ public function setUpDirectoryListInstallation() { $this->setUpDirectoryListInstallationInProduction(); $this->directoryListMock - ->expects($this->at(4)) + ->expects($this->at(3)) ->method('getPath') ->with(DirectoryList::GENERATED) ->willReturn(BP . '/generated'); + $this->directoryListMock + ->expects($this->at(4)) + ->method('getPath') + ->with(DirectoryList::STATIC_VIEW) + ->willReturn(BP . '/pub/static'); } public function setUpDirectoryListInstallationInProduction() @@ -306,11 +309,6 @@ public function setUpDirectoryListInstallationInProduction() ->method('getPath') ->with(DirectoryList::MEDIA) ->willReturn(BP . '/pub/media'); - $this->directoryListMock - ->expects($this->at(3)) - ->method('getPath') - ->with(DirectoryList::STATIC_VIEW) - ->willReturn(BP . '/pub/static'); } public function setUpDirectoryWriteInstallation() @@ -348,24 +346,6 @@ public function setUpDirectoryWriteInstallation() ->expects($this->at(6)) ->method('isDirectory') ->willReturn(false); - - // STATIC_VIEW - $this->directoryWriteMock - ->expects($this->at(7)) - ->method('isExist') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(8)) - ->method('isDirectory') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(9)) - ->method('isReadable') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(10)) - ->method('isWritable') - ->willReturn(false); } /** From a22010091673e485be42a2bd8097488d241da254 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Thu, 24 Sep 2020 09:31:06 +0300 Subject: [PATCH 045/195] MC-35016: Out of stock products doesn't filter properly using "price" filter --- ...seSelectProcessor.php => BaseStockStatusSelectProcessor.php} | 2 +- app/code/Magento/ConfigurableProduct/etc/di.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/{StockStatusBaseSelectProcessor.php => BaseStockStatusSelectProcessor.php} (96%) diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php similarity index 96% rename from app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php rename to app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php index b5cbaa57858c9..cbeaf2cea90e0 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php @@ -18,7 +18,7 @@ * * Adds stock status limitations to a given Select object. */ -class StockStatusBaseSelectProcessor implements BaseSelectProcessorInterface +class BaseStockStatusSelectProcessor implements BaseSelectProcessorInterface { /** * @var ResourceConnection diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 9f01af66f9713..c7f67a69d669f 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -198,7 +198,7 @@ Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy indexer - Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\StockStatusBaseSelectProcessor + Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\BaseStockStatusSelectProcessor From 9713cde24c81683269bd0495330b013feca5e683 Mon Sep 17 00:00:00 2001 From: Oleh Usik Date: Mon, 28 Sep 2020 23:19:18 +0300 Subject: [PATCH 046/195] add new ReloadPageActionGroup --- .../Test/AdminCheckAnalyticsTrackingTest.xml | 3 +-- .../Mftf/Test/AdminExpireAdminSessionTest.xml | 2 +- .../Test/AdminExpireCustomerSessionTest.xml | 2 +- .../CaptchaWithDisabledGuestCheckoutTest.xml | 3 +-- ...egoryIndexerInUpdateOnScheduleModeTest.xml | 4 ++-- ...rontCatalogNavigationMenuUIDesktopTest.xml | 12 ++++-------- ...UKCustomerRemainOptionAfterRefreshTest.xml | 3 +-- ...sNotAffectedStartedCheckoutProcessTest.xml | 3 +-- ...tCheckoutUsingFreeShippingAndTaxesTest.xml | 3 +-- ...aForGuestCustomerWithPhysicalQuoteTest.xml | 6 ++---- ...riceInShoppingCartAfterProductSaveTest.xml | 3 +-- ...efrontVisibilityOfDuplicateProductTest.xml | 6 ++---- .../Mftf/Test/AdminCreateCustomerTest.xml | 3 +-- ...dAreaSessionMustNotAffectAdminAreaTest.xml | 6 ++---- ...ingCartBehaviorAfterSessionExpiredTest.xml | 3 +-- ...efrontGuestCheckoutDisabledProductTest.xml | 6 ++---- ...ateCartPriceRuleForGeneratedCouponTest.xml | 3 +-- .../StorefrontAutoGeneratedCouponCodeTest.xml | 3 +-- .../Test/AdminDisablingSwatchTooltipsTest.xml | 3 +-- ...refrontInlineTranslationOnCheckoutTest.xml | 6 ++---- .../ActionGroup/ReloadPageActionGroup.xml | 19 +++++++++++++++++++ 21 files changed, 48 insertions(+), 54 deletions(-) create mode 100644 app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml index 4f0e9bb000a27..99c60eba67854 100644 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml @@ -22,14 +22,13 @@ - - + diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml index 2469151337bfe..b87b92e86528c 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml @@ -29,7 +29,7 @@ - + diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml index 0e3bf07d32441..b2b71c4ad3eca 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml @@ -40,7 +40,7 @@ - + diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml index 9e99fa96ee766..39a774369c331 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -46,8 +46,7 @@ - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index eebd3472cbd95..4eac36c28ab98 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -99,7 +99,7 @@ - + @@ -128,7 +128,7 @@ - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index 2a59be6306a30..07c67f6f290f1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -40,8 +40,7 @@ - - + @@ -87,8 +86,7 @@ - - + @@ -167,8 +165,7 @@ - - + @@ -203,8 +200,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index e7e8f9f0ef699..de4e64e3c5938 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -42,8 +42,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index a1065daedd4f8..3128387e4155b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -90,8 +90,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 5a0610f5c5b0a..aa05a4828e555 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -172,8 +172,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index e42d5e1bae956..6ac85e77766e1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -49,8 +49,7 @@ - - + @@ -71,8 +70,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index a7a0917532dcb..299d33244f1fb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -62,8 +62,7 @@ - - + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 976be77122547..a066d5077f713 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -62,8 +62,7 @@ - - + @@ -142,8 +141,7 @@ - - + diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index cb003ed837294..710e4bba29e05 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -40,8 +40,7 @@ - - + diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index 1c280acd63a7b..9eb0558bdfd2e 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -76,10 +76,8 @@ - - - - + + diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml index 18e19c4276548..533a06986b70a 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml @@ -55,8 +55,7 @@ - - + diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 80af412439338..92a1c1facd6a6 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -124,8 +124,7 @@ - - + @@ -151,8 +150,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index e18a9eaadcd23..953d142a49ab1 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -60,8 +60,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index c2aeca657db3b..631c516153fa2 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -64,8 +64,7 @@ - - + diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index b48f181c8d199..8ad9578e9184a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -153,8 +153,7 @@ - - + diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index e30ab98982b78..cfee0785ac1d1 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -120,8 +120,7 @@ - - + @@ -490,8 +489,7 @@ - - + diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml new file mode 100644 index 0000000000000..3976a2ac0f872 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + Reload page and wait for page load. + + + + + + From 1ca6ef5ee628d8329850ba93e90ff7ea2ae8d648 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh Date: Wed, 30 Sep 2020 12:51:49 +0300 Subject: [PATCH 047/195] MC-37718: Grouped product remains In Stock On Mass Update --- ...ateProductQtyAndStockStatusActionGroup.xml | 30 ++++ .../Data/ProductAttributeMassUpdateData.xml | 8 + ...dateAttributesAdvancedInventorySection.xml | 17 +++ .../Plugin/MassUpdateProductAttribute.php | 38 ++++- .../Model/Inventory/ParentItemProcessor.php | 143 +----------------- .../UpdateStockStatusGroupedProductTest.xml | 65 ++++++++ app/code/Magento/GroupedProduct/etc/di.xml | 7 + 7 files changed, 169 insertions(+), 139 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml new file mode 100644 index 0000000000000..fce287705b67c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml index 99908f1c9df5f..22557972b991f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml @@ -12,4 +12,12 @@ New Bundle Product Name This is the description + + 10 + In Stock + + + 0 + Out of Stock + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml new file mode 100644 index 0000000000000..92dadbdd26c2d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml @@ -0,0 +1,17 @@ + + + +
+ + + + + +
+
diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index 334d2b22edbfa..b562171c95bff 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -5,11 +5,15 @@ */ namespace Magento\CatalogInventory\Plugin; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; +use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; /** - * MassUpdate product attribute. + * Around plugin for MassUpdate product attribute via product grid. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MassUpdateProductAttribute @@ -49,6 +53,15 @@ class MassUpdateProductAttribute */ private $messageManager; + /** + * @var ParentItemProcessorInterface[] + */ + private $parentItemProcessors; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper @@ -57,6 +70,8 @@ class MassUpdateProductAttribute * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @param ProductRepositoryInterface $productRepository + * @param ParentItemProcessorInterface[] $parentItemProcessors * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -66,7 +81,9 @@ public function __construct( \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, - \Magento\Framework\Message\ManagerInterface $messageManager + \Magento\Framework\Message\ManagerInterface $messageManager, + ProductRepositoryInterface $productRepository, + array $parentItemProcessors = [] ) { $this->stockIndexerProcessor = $stockIndexerProcessor; $this->dataObjectHelper = $dataObjectHelper; @@ -75,6 +92,8 @@ public function __construct( $this->stockConfiguration = $stockConfiguration; $this->attributeHelper = $attributeHelper; $this->messageManager = $messageManager; + $this->productRepository = $productRepository; + $this->parentItemProcessors = $parentItemProcessors; } /** @@ -145,6 +164,7 @@ private function addConfigSettings($inventoryData) private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void { foreach ($productIds as $productId) { + $product = $this->productRepository->getById($productId); $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); if (!$stockItemDo->getProductId()) { $inventoryData['product_id'] = $productId; @@ -153,7 +173,21 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); $stockItemDo->setItemId($stockItemId); $this->stockItemRepository->save($stockItemDo); + $this->processParents($product); } $this->stockIndexerProcessor->reindexList($productIds); } + + /** + * Process stock data for parent products + * + * @param Product $product + * @return void + */ + private function processParents(Product $product): void + { + foreach ($this->parentItemProcessors as $processor) { + $processor->process($product); + } + } } diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php index 0bb102f34dd2d..2d5113edd082e 100644 --- a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php @@ -7,17 +7,8 @@ namespace Magento\GroupedProduct\Model\Inventory; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\GroupedProduct\Model\Product\Type\Grouped; use Magento\Catalog\Api\Data\ProductInterface as Product; -use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; -use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; -use Magento\CatalogInventory\Api\Data\StockItemInterface; -use Magento\GroupedProduct\Model\ResourceModel\Product\Link; -use Magento\Framework\App\ResourceConnection; /** * Process parent stock item for grouped product @@ -25,59 +16,17 @@ class ParentItemProcessor implements ParentItemProcessorInterface { /** - * @var Grouped + * @var ChangeParentStockStatus */ - private $groupedType; + private $changeParentStockStatus; /** - * @var StockItemRepositoryInterface - */ - private $stockItemRepository; - - /** - * @var StockConfigurationInterface - */ - private $stockConfiguration; - - /** - * @var StockItemCriteriaInterfaceFactory - */ - private $criteriaInterfaceFactory; - - /** - * Product metadata pool - * - * @var MetadataPool - */ - private $metadataPool; - - /** - * @var ResourceConnection - */ - private $resource; - - /** - * @param Grouped $groupedType - * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory - * @param StockItemRepositoryInterface $stockItemRepository - * @param StockConfigurationInterface $stockConfiguration - * @param ResourceConnection $resource - * @param MetadataPool $metadataPool + * @param ChangeParentStockStatus $changeParentStockStatus */ public function __construct( - Grouped $groupedType, - StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, - StockItemRepositoryInterface $stockItemRepository, - StockConfigurationInterface $stockConfiguration, - ResourceConnection $resource, - MetadataPool $metadataPool + ChangeParentStockStatus $changeParentStockStatus ) { - $this->groupedType = $groupedType; - $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; - $this->stockConfiguration = $stockConfiguration; - $this->stockItemRepository = $stockItemRepository; - $this->resource = $resource; - $this->metadataPool = $metadataPool; + $this->changeParentStockStatus = $changeParentStockStatus; } /** @@ -88,86 +37,6 @@ public function __construct( */ public function process(Product $product) { - $parentIds = $this->getParentEntityIdsByChild($product->getId()); - foreach ($parentIds as $productId) { - $this->processStockForParent((int)$productId); - } - } - - /** - * Change stock item for parent product depending on children stock items - * - * @param int $productId - * @return void - */ - private function processStockForParent(int $productId) - { - $criteria = $this->criteriaInterfaceFactory->create(); - $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); - $criteria->setProductsFilter($productId); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - $allItems = $stockItemCollection->getItems(); - if (empty($allItems)) { - return; - } - $parentStockItem = array_shift($allItems); - $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); - $criteria->setProductsFilter($groupedChildrenIds); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - $allItems = $stockItemCollection->getItems(); - - $groupedChildrenIsInStock = false; - - foreach ($allItems as $childItem) { - if ($childItem->getIsInStock() === true) { - $groupedChildrenIsInStock = true; - break; - } - } - - if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { - $parentStockItem->setIsInStock($groupedChildrenIsInStock); - $parentStockItem->setStockStatusChangedAuto(1); - $this->stockItemRepository->save($parentStockItem); - } - } - - /** - * Check is parent item should be updated - * - * @param StockItemInterface $parentStockItem - * @param bool $childrenIsInStock - * @return bool - */ - private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool - { - return $parentStockItem->getIsInStock() !== $childrenIsInStock && - ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); - } - - /** - * Retrieve parent ids array by child id - * - * @param int $childId - * @return string[] - */ - private function getParentEntityIdsByChild($childId) - { - $select = $this->resource->getConnection() - ->select() - ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) - ->join( - ['e' => $this->resource->getTableName('catalog_product_entity')], - 'e.' . - $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', - ['e.entity_id'] - ) - ->where('l.linked_product_id = ?', $childId) - ->where( - 'link_type_id = ?', - Link::LINK_TYPE_GROUPED - ); - - return $this->resource->getConnection()->fetchCol($select); + $this->changeParentStockStatus->execute((int)$product->getId()); } } diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml new file mode 100644 index 0000000000000..12d753c300ca5 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -0,0 +1,65 @@ + + + + + + + + + + <description value="Change stock of grouped product after changing quantity of child product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38057"/> + <useCaseId value="MC-37718"/> + <group value="GroupedProduct"/> + </annotations> + <before> + <!--Create simple and grouped product--> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + <!--Admin logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--1.Open product grid page and choose "Update attributes" and set product stock status to "Out of Stock"--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridFirstTime"/> + <waitForPageLoad stepKey="waitForProductGridFirstTime"/> + <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="setProductToOutOfStock"> + <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> + </actionGroup> + <!--2.Run cron for updating stock status of parent product--> + <magentoCron stepKey="runAllCronJobs"/> + <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> + <argument name="productId" value="$$createGroupedProduct.id$$"/> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <!--4.Open product grid page choose "Update attributes" and set product stock status to "In Stock"--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridSecondTime"/> + <waitForPageLoad stepKey="waitForProductGridSecondTime"/> + <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="returnProductToInStock"> + <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> + </actionGroup> + <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> + <argument name="productId" value="$$createGroupedProduct.id$$"/> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index d9534c6d3fe7d..924d2d1fc9669 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -112,4 +112,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="grouped" xsi:type="object"> Magento\GroupedProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> </config> From 0d4e154c9532566c9ca5c9121992e03a778742a1 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 16:13:58 +0300 Subject: [PATCH 048/195] MC-37718: Grouped product remains In Stock On Mass Update --- .../Inventory/ChangeParentStockStatus.php | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php b/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php new file mode 100644 index 0000000000000..bf1f6c1a5cf1a --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Inventory; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\GroupedProduct\Model\ResourceModel\Product\Link; + +/** + * Change stock status of grouped product by child product id + */ +class ChangeParentStockStatus +{ + /** + * @var Grouped + */ + private $groupedType; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * Product metadata pool + * + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param Grouped $groupedType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + * @param ResourceConnection $resource + * @param MetadataPool $metadataPool + */ + public function __construct( + Grouped $groupedType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration, + ResourceConnection $resource, + MetadataPool $metadataPool + ) { + $this->groupedType = $groupedType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockItemRepository = $stockItemRepository; + $this->resource = $resource; + $this->metadataPool = $metadataPool; + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + public function execute(int $productId): void + { + $parentIds = $this->getParentEntityIdsByChild($productId); + foreach ($parentIds as $productId) { + $this->changeParentStockStatus((int)$productId); + } + } + + /** + * Change stock status of grouped product + * + * @param int $productId + * @return void + */ + private function changeParentStockStatus(int $productId): void + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); + $criteria->setProductsFilter($groupedChildrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $groupedChildrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $groupedChildrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { + $parentStockItem->setIsInStock($groupedChildrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool + { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } + + /** + * Retrieve parent ids array by child id + * + * @param int $childId + * @return array + */ + private function getParentEntityIdsByChild(int $childId): array + { + $select = $this->resource->getConnection() + ->select() + ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) + ->join( + ['e' => $this->resource->getTableName('catalog_product_entity')], + 'e.' . + $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', + ['e.entity_id'] + ) + ->where('l.linked_product_id = ?', $childId) + ->where( + 'link_type_id = ?', + Link::LINK_TYPE_GROUPED + ); + + return $this->resource->getConnection()->fetchCol($select); + } +} From f692a937080862f3db77275c03b8f2edc27b45bb Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 19:00:01 +0300 Subject: [PATCH 049/195] MC-37718: Grouped product remains In Stock On Mass Update --- ...pdateProductQtyAndStockStatusActionGroup.xml | 17 ++++++++++++++++- .../UpdateStockStatusGroupedProductTest.xml | 6 ++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml index fce287705b67c..4b5aca5050858 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml @@ -8,16 +8,28 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <!-- Update Product Name and Description attribute --> + <!--Update Product Name and Description attribute--> <actionGroup name="AdminMassUpdateProductQtyAndStockStatusActionGroup"> <arguments> <argument name="attributes"/> + <argument name="product"/> </arguments> + <!--Filter product in product grid--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageFirstTime"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <selectOption selector="{{AdminProductGridFilterSection.typeFilter}}" userInput="{{product.type_id}}" stepKey="selectionProductType"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <!--Select first product from grid and open mass action--> <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox"/> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> <waitForPageLoad stepKey="waitForUploadPage"/> <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeAttributePageEditUrl"/> + <!--Update inventory attributes and save--> <click selector="{{AdminUpdateAttributesAdvancedInventorySection.inventory}}" stepKey="openInvetoryTab"/> <click selector="{{AdminUpdateAttributesAdvancedInventorySection.changeQty}}" stepKey="uncheckChangeQty"/> <fillField selector="{{AdminUpdateAttributesAdvancedInventorySection.qty}}" userInput="{{attributes.qty}}" stepKey="fillFieldName"/> @@ -26,5 +38,8 @@ <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="save"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitVisibleSuccessMessage"/> <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageSecondTime"/> + <waitForPageLoad stepKey="waitForProductGridPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersAfterMassAction"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index 12d753c300ca5..cb9434029ff2e 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -38,10 +38,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> <!--1.Open product grid page and choose "Update attributes" and set product stock status to "Out of Stock"--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridFirstTime"/> - <waitForPageLoad stepKey="waitForProductGridFirstTime"/> <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="setProductToOutOfStock"> <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> + <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> <!--2.Run cron for updating stock status of parent product--> <magentoCron stepKey="runAllCronJobs"/> @@ -51,10 +50,9 @@ <argument name="stockStatus" value="Out of Stock"/> </actionGroup> <!--4.Open product grid page choose "Update attributes" and set product stock status to "In Stock"--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridSecondTime"/> - <waitForPageLoad stepKey="waitForProductGridSecondTime"/> <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="returnProductToInStock"> <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> + <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> From 36969046e0cf1a2abeccad598cd9a84185cb1e5a Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 21:10:31 +0300 Subject: [PATCH 050/195] MC-37718: Grouped product remains In Stock On Mass Update --- .../Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index cb9434029ff2e..bf071f568a376 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -43,7 +43,8 @@ <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> <!--2.Run cron for updating stock status of parent product--> - <magentoCron stepKey="runAllCronJobs"/> + <magentoCLI command="cron:run" stepKey="runCron"/> + <wait time="60" stepKey="waitForChanges"/> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> From 9914179daa12e9a6175fc4125b2f92084cc04de2 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 1 Oct 2020 13:25:36 +0300 Subject: [PATCH 051/195] MC-37718: Grouped product remains In Stock On Mass Update --- .../Plugin/MassUpdateProductAttribute.php | 8 +++++--- .../Test/Mftf/Data/QueueConsumerData.xml | 15 +++++++++++++++ .../Test/UpdateStockStatusGroupedProductTest.xml | 10 ++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index b562171c95bff..ee5f16989ba37 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -3,11 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Plugin; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; -use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; @@ -181,10 +183,10 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa /** * Process stock data for parent products * - * @param Product $product + * @param ProductInterface $product * @return void */ - private function processParents(Product $product): void + private function processParents(ProductInterface $product): void { foreach ($this->parentItemProcessors as $processor) { $processor->process($product); diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml new file mode 100644 index 0000000000000..ef58de6cdc496 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminInventoryMassUpdateConsumerData"> + <data key="consumerName">inventory.mass.update</data> + <data key="messageLimit">10</data> + </entity> +</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index bf071f568a376..42ffe88beb46e 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -27,7 +27,7 @@ <requiredEntity createDataKey="createGroupedProduct"/> <requiredEntity createDataKey="createFirstSimpleProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCron groups="index" stepKey="runCronIndex"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -42,9 +42,11 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--2.Run cron for updating stock status of parent product--> - <magentoCLI command="cron:run" stepKey="runCron"/> - <wait time="60" stepKey="waitForChanges"/> + <!--2.Run cron for start consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> + </actionGroup> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> From d6750b5890a4b4d5362723180c7da871ff814980 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 1 Oct 2020 15:16:47 +0300 Subject: [PATCH 052/195] MC-37718: Grouped product remains In Stock On Mass Update --- .../Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index 42ffe88beb46e..1d2d5333952ae 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -57,7 +57,12 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> + <!--5.Run cron for start consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueueSecond"> + <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> + </actionGroup> + <!--6.Check stock status of grouped product. Stock status should be "In Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> <argument name="stockStatus" value="In Stock"/> From 5c05c30ccedbf71a78fd30e742b50218c0cb0c46 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 1 Oct 2020 20:13:34 +0300 Subject: [PATCH 053/195] MC-37718: Grouped product remains In Stock On Mass Update --- .../Test/Mftf/Data/QueueConsumerData.xml | 15 --------------- .../Test/UpdateStockStatusGroupedProductTest.xml | 14 +++----------- 2 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml deleted file mode 100644 index ef58de6cdc496..0000000000000 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="AdminInventoryMassUpdateConsumerData"> - <data key="consumerName">inventory.mass.update</data> - <data key="messageLimit">10</data> - </entity> -</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index 1d2d5333952ae..f39e18373893d 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -42,11 +42,8 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--2.Run cron for start consumer --> - <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> - <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> - <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> - </actionGroup> + <!--2.Run cron for updating stock status of parent product--> + <magentoCron groups="index" stepKey="runCronIndex"/> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> @@ -57,12 +54,7 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--5.Run cron for start consumer --> - <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueueSecond"> - <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> - <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> - </actionGroup> - <!--6.Check stock status of grouped product. Stock status should be "In Stock"--> + <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> <argument name="stockStatus" value="In Stock"/> From 9804ad90d9b7d1ada7fe89a782dcec49dc892914 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Fri, 2 Oct 2020 16:42:47 -0500 Subject: [PATCH 054/195] [AWS S3] MC-37479: Support by Magento Content Design (#6179) * MC-37479: Support by Magento Content Design --- app/code/Magento/AwsS3/Driver/AwsS3.php | 209 ++++++----- .../Magento/AwsS3/Driver/AwsS3Factory.php | 45 ++- app/code/Magento/AwsS3/Model/Config.php | 36 +- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 327 ++++++++++++++++++ .../Command/RemoteStorageEnableCommand.php | 90 +++++ .../RemoteStorage/Driver/DriverPool.php | 47 ++- app/code/Magento/RemoteStorage/Filesystem.php | 108 ++++++ .../Magento/RemoteStorage/Model/Config.php | 44 ++- .../Model/Config/Source/FileStorage.php | 37 -- .../RemoteStorage/Model/Filesystem.php | 55 --- .../Magento/RemoteStorage/Plugin/Scope.php | 63 ++++ .../Magento/RemoteStorage/Plugin/Sitemap.php | 23 +- .../Test/Unit/Model/ConfigTest.php | 43 --- .../RemoteStorage/etc/adminhtml/di.xml | 19 - .../RemoteStorage/etc/adminhtml/system.xml | 20 -- app/code/Magento/RemoteStorage/etc/di.xml | 42 ++- app/code/Magento/RemoteStorage/etc/module.xml | 1 + .../Theme/Model/Design/Backend/File.php | 2 +- .../Magento/Framework/File/Uploader.php | 25 +- 19 files changed, 903 insertions(+), 333 deletions(-) create mode 100644 app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php create mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php create mode 100644 app/code/Magento/RemoteStorage/Filesystem.php delete mode 100644 app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php delete mode 100644 app/code/Magento/RemoteStorage/Model/Filesystem.php create mode 100644 app/code/Magento/RemoteStorage/Plugin/Scope.php delete mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php delete mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/di.xml delete mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/system.xml diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 602e81ec480ff..a224d9d6ce0ef 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -7,7 +7,6 @@ namespace Magento\AwsS3\Driver; -use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; @@ -18,10 +17,10 @@ */ class AwsS3 implements DriverInterface { - public const S3 = 'aws-s3'; + public const TYPE_DIR = 'dir'; + public const TYPE_FILE = 'file'; - private const TYPE_DIR = 'dir'; - private const TYPE_FILE = 'file'; + private const CONFIG = ['ACL' => 'public-read']; /** * @var AwsS3Adapter @@ -34,27 +33,11 @@ class AwsS3 implements DriverInterface private $streams = []; /** - * @param string $region - * @param string $bucket - * @param string|null $key - * @param string|null $secret + * @param AwsS3Adapter $adapter */ - public function __construct(string $region, string $bucket, string $key = null, string $secret = null) + public function __construct(AwsS3Adapter $adapter) { - $config = [ - 'region' => $region, - 'version' => 'latest' - ]; - - if ($key && $secret) { - $config['credentials'] = [ - 'key' => $key, - 'secret' => $secret, - ]; - } - - $client = new S3Client($config); - $this->adapter = new AwsS3Adapter($client, $bucket); + $this->adapter = $adapter; } /** @@ -74,7 +57,7 @@ public function __destruct() */ public function fileGetContents($path, $flag = null, $context = null): string { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (isset($this->streams[$path])) { //phpcs:disable @@ -82,7 +65,7 @@ public function fileGetContents($path, $flag = null, $context = null): string //phpcs:enable } - return $this->adapter->read($path)['contents']; + return $this->adapter->read($path)['contents'] ?? ''; } /** @@ -94,7 +77,7 @@ public function isExists($path): bool return true; } - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (!$path || $path === '/') { return true; @@ -120,7 +103,26 @@ public function createDirectory($path, $permissions = 0777): bool return true; } - $path = $this->getRelativePath('', $path); + return $this->createDirectoryRecursively( + $this->normalizeRelativePath($path) + ); + } + + /** + * Created directory recursively. + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + private function createDirectoryRecursively(string $path): bool + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $parentDir = dirname($path); + + while (!$this->isDirectory($parentDir)) { + $this->createDirectoryRecursively($parentDir); + } return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config([])); } @@ -130,10 +132,10 @@ public function createDirectory($path, $permissions = 0777): bool */ public function copy($source, $destination, DriverInterface $targetDriver = null): bool { - $source = $this->getRelativePath('', $source); - $destination = $this->getRelativePath('', $destination); - - return $this->adapter->copy($source, $destination); + return $this->adapter->copy( + $this->normalizeRelativePath($source), + $this->normalizeRelativePath($destination) + ); } /** @@ -141,9 +143,9 @@ public function copy($source, $destination, DriverInterface $targetDriver = null */ public function deleteFile($path): bool { - $path = $this->getRelativePath('', $path); - - return $this->adapter->delete($path); + return $this->adapter->delete( + $this->normalizeRelativePath($path) + ); } /** @@ -151,9 +153,9 @@ public function deleteFile($path): bool */ public function deleteDirectory($path): bool { - $path = $this->getRelativePath('', $path); - - return $this->adapter->deleteDir($path); + return $this->adapter->deleteDir( + $this->normalizeRelativePath($path) + ); } /** @@ -161,9 +163,9 @@ public function deleteDirectory($path): bool */ public function filePutContents($path, $content, $mode = null, $context = null): int { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); - return $this->adapter->write($path, $content, new Config(['ACL' => 'public-read']))['size']; + return $this->adapter->write($path, $content, new Config(self::CONFIG))['size']; } /** @@ -171,9 +173,10 @@ public function filePutContents($path, $content, $mode = null, $context = null): */ public function readDirectoryRecursively($path = null): array { - $path = $this->getRelativePath('', $path); - - return $this->adapter->listContents($path, true); + return $this->adapter->listContents( + $this->normalizeRelativePath($path), + true + ); } /** @@ -181,9 +184,10 @@ public function readDirectoryRecursively($path = null): array */ public function readDirectory($path): array { - $path = $this->getRelativePath('', $path); - - return $this->adapter->listContents($path, false); + return $this->adapter->listContents( + $this->normalizeRelativePath($path), + false + ); } /** @@ -191,7 +195,9 @@ public function readDirectory($path): array */ public function getRealPathSafety($path) { - return '/'; + return $this->normalizeAbsolutePath( + $this->normalizeRelativePath($path) + ); } /** @@ -199,19 +205,52 @@ public function getRealPathSafety($path) */ public function getAbsolutePath($basePath, $path, $scheme = null) { - $path = $this->getRelativePath($basePath, $path); + if ($basePath && $path && 0 === strpos($path, $basePath)) { + return $this->normalizeAbsolutePath( + $this->normalizeRelativePath($path) + ); + } - if ($path === '/') { - $path = ''; + if ($basePath && $basePath !== '/') { + return $basePath . ltrim((string)$path, '/'); } - if ($basePath !== '/') { - $path = $basePath . $path; + return $this->normalizeAbsolutePath($path); + } + + /** + * Resolves absolute path. + * + * @param string $path Relative path + * @return string Absolute path + */ + private function normalizeAbsolutePath(string $path = '.'): string + { + $path = ltrim($path, '/'); + + if (!$path) { + $path = '.'; } - $path = $path ?: '.'; + return $this->adapter->getClient()->getObjectUrl( + $this->adapter->getBucket(), + $this->adapter->applyPathPrefix($path) + ); + } - return $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), $path); + /** + * Resolves relative path. + * + * @param string $path Absolute path + * @return string Relative path + */ + private function normalizeRelativePath(string $path): string + { + return str_replace( + $this->normalizeAbsolutePath(), + '', + $path + ); } /** @@ -227,11 +266,11 @@ public function isReadable($path): bool */ public function isFile($path): bool { - if ($path === '/') { + if (!$path || $path === '/') { return false; } - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); $path = rtrim($path, '/'); return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_FILE; @@ -242,11 +281,11 @@ public function isFile($path): bool */ public function isDirectory($path): bool { - if ($path === '/') { + if (in_array($path, ['.', '/'], true)) { return true; } - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (!$path || $path === '/') { return true; @@ -262,23 +301,14 @@ public function isDirectory($path): bool */ public function getRelativePath($basePath, $path = null): string { - $relativePath = str_replace( - $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), '.'), - '', - $path - ); + $basePath = $this->normalizeAbsolutePath($basePath); + $absolutePath = $this->normalizeAbsolutePath((string)$path); - if ($basePath && $basePath !== '/') { - $relativePath = str_replace($basePath, '', $relativePath); + if ($basePath === $absolutePath . '/' || strpos($absolutePath, $basePath) === 0) { + return ltrim(substr($absolutePath, strlen($basePath)), '/'); } - $relativePath = ltrim($relativePath, '/'); - - if (!$relativePath) { - $relativePath = '/'; - } - - return $relativePath; + return ltrim($path, '/'); } /** @@ -286,7 +316,8 @@ public function getRelativePath($basePath, $path = null): string */ public function getParentDirectory($path): string { - return '/'; + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return dirname($this->normalizeAbsolutePath($path)); } /** @@ -294,7 +325,7 @@ public function getParentDirectory($path): string */ public function getRealPath($path) { - return $this->getAbsolutePath('', $path); + return $this->normalizeAbsolutePath($path); } /** @@ -302,10 +333,10 @@ public function getRealPath($path) */ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null): bool { - $oldPath = $this->getRelativePath('', $oldPath); - $newPath = $this->getRelativePath('', $newPath); - - return $this->adapter->rename($oldPath, $newPath); + return $this->adapter->rename( + $this->normalizeRelativePath($oldPath), + $this->normalizeRelativePath($newPath) + ); } /** @@ -313,7 +344,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) */ public function stat($path): array { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); $metaInfo = $this->adapter->getMetadata($path); if (!$metaInfo) { @@ -336,6 +367,7 @@ public function stat($path): array 'type' => $metaInfo['type'], 'mtime' => $metaInfo['timestamp'], 'disposition' => null, + 'mimetype' => $metaInfo['mimetype'] ]; } @@ -368,7 +400,7 @@ public function changePermissions($path, $permissions): bool */ public function changePermissionsRecursively($path, $dirPermissions, $filePermissions): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return true; } /** @@ -376,7 +408,13 @@ public function changePermissionsRecursively($path, $dirPermissions, $filePermis */ public function touch($path, $modificationTime = null) { - return true; + $path = $this->normalizeRelativePath($path); + + $content = $this->adapter->has($path) ? + $this->adapter->read($path)['contents'] + : ''; + + return (bool)$this->adapter->write($path, $content, new Config([])); } /** @@ -478,13 +516,15 @@ public function fileWrite($resource, $data) { //phpcs:disable $resourcePath = stream_get_meta_data($resource)['uri']; + //phpcs:enable foreach ($this->streams as $stream) { + //phpcs:disable if (stream_get_meta_data($stream)['uri'] === $resourcePath) { return fwrite($stream, $data); } + //phpcs:enable } - //phpcs:enable return false; } @@ -496,10 +536,12 @@ public function fileClose($resource): bool { //phpcs:disable $resourcePath = stream_get_meta_data($resource)['uri']; + //phpcs:enable foreach ($this->streams as $path => $stream) { + //phpcs:disable if (stream_get_meta_data($stream)['uri'] === $resourcePath) { - $this->adapter->writeStream($path, $resource, new Config(['ACL' => 'public-read'])); + $this->adapter->writeStream($path, $resource, new Config(self::CONFIG)); // Remove path from streams after unset($this->streams[$path]); @@ -507,7 +549,6 @@ public function fileClose($resource): bool return fclose($stream); } } - //phpcs:enable return false; } @@ -517,7 +558,7 @@ public function fileClose($resource): bool */ public function fileOpen($path, $mode) { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (!isset($this->streams[$path])) { $this->streams[$path] = tmpfile(); diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index e71c3a84d3ce5..6ab8f93fa6e2b 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -7,9 +7,11 @@ namespace Magento\AwsS3\Driver; +use Aws\S3\S3Client; +use League\Flysystem\AwsS3v3\AwsS3Adapter; use Magento\AwsS3\Model\Config; - use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\RemoteStorage\Driver\DriverFactoryInterface; /** @@ -17,16 +19,23 @@ */ class AwsS3Factory implements DriverFactoryInterface { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * @var Config */ private $config; /** + * @param ObjectManagerInterface $objectManager * @param Config $config */ - public function __construct(Config $config) + public function __construct(ObjectManagerInterface $objectManager, Config $config) { + $this->objectManager = $objectManager; $this->config = $config; } @@ -37,11 +46,33 @@ public function __construct(Config $config) */ public function create(): DriverInterface { - return new AwsS3( - $this->config->getRegion(), - $this->config->getBucket(), - $this->config->getAccessKey(), - $this->config->getSecretKey() + $config = [ + 'region' => $this->config->getRegion(), + 'version' => 'latest' + ]; + + $key = $this->config->getAccessKey(); + $secret = $this->config->getSecretKey(); + + if ($key && $secret) { + $config['credentials'] = [ + 'key' => $key, + 'secret' => $secret, + ]; + } + + return $this->objectManager->create( + AwsS3::class, + [ + 'adapter' => $this->objectManager->create( + AwsS3Adapter::class, + [ + 'client' => $this->objectManager->create(S3Client::class, ['args' => $config]), + 'bucket' => $this->config->getBucket(), + 'prefix' => $this->config->getPrefix() + ] + ) + ] ); } } diff --git a/app/code/Magento/AwsS3/Model/Config.php b/app/code/Magento/AwsS3/Model/Config.php index 00cd5b36740e6..f4e19edd4eec3 100644 --- a/app/code/Magento/AwsS3/Model/Config.php +++ b/app/code/Magento/AwsS3/Model/Config.php @@ -7,28 +7,28 @@ namespace Magento\AwsS3\Model; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; /** * Configuration for AWS S3. */ class Config { - public const PATH_DRIVER = 'system/file_system/driver'; - public const PATH_REGION = 'system/file_system/region'; - public const PATH_BUCKET = 'system/file_system/bucket'; - public const PATH_ACCESS_KEY = 'system/file_system/access_key'; - public const PATH_SECRET_KEY = 'system/file_system/secret_key'; + public const PATH_REGION = 'remote_storage/region'; + public const PATH_BUCKET = 'remote_storage/bucket'; + public const PATH_ACCESS_KEY = 'remote_storage/access_key'; + public const PATH_SECRET_KEY = 'remote_storage/secret_key'; + public const PATH_PREFIX = 'remote_storage/prefix'; /** - * @var ScopeConfigInterface + * @var DeploymentConfig */ private $config; /** - * @param ScopeConfigInterface $config + * @param DeploymentConfig $config */ - public function __construct(ScopeConfigInterface $config) + public function __construct(DeploymentConfig $config) { $this->config = $config; } @@ -40,7 +40,7 @@ public function __construct(ScopeConfigInterface $config) */ public function getRegion(): string { - return (string)$this->config->getValue(self::PATH_REGION); + return (string)$this->config->get(self::PATH_REGION); } /** @@ -50,7 +50,7 @@ public function getRegion(): string */ public function getBucket(): string { - return (string)$this->config->getValue(self::PATH_BUCKET); + return (string)$this->config->get(self::PATH_BUCKET); } /** @@ -60,7 +60,7 @@ public function getBucket(): string */ public function getAccessKey(): string { - return (string)$this->config->getValue(self::PATH_ACCESS_KEY); + return (string)$this->config->get(self::PATH_ACCESS_KEY); } /** @@ -70,6 +70,16 @@ public function getAccessKey(): string */ public function getSecretKey(): string { - return (string)$this->config->getValue(self::PATH_SECRET_KEY); + return (string)$this->config->get(self::PATH_SECRET_KEY); + } + + /** + * Retrieves prefix. + * + * @return string + */ + public function getPrefix(): string + { + return (string)$this->config->get(self::PATH_PREFIX, ''); } } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php new file mode 100644 index 0000000000000..5ddc4811230ec --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -0,0 +1,327 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use Aws\S3\S3ClientInterface; +use League\Flysystem\AwsS3v3\AwsS3Adapter; +use Magento\AwsS3\Driver\AwsS3; +use Magento\Framework\Exception\FileSystemException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @see AwsS3 + */ +class AwsS3Test extends TestCase +{ + private const URL = 'https://test.s3.amazonaws.com/'; + + /** + * @var AwsS3 + */ + private $driver; + + /** + * @var AwsS3Adapter|MockObject + */ + private $adapterMock; + + /** + * @var S3ClientInterface|MockObject + */ + private $clientMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->adapterMock = $this->createMock(AwsS3Adapter::class); + $this->clientMock = $this->getMockForAbstractClass(S3ClientInterface::class); + + $this->adapterMock->method('applyPathPrefix') + ->willReturnArgument(0); + $this->adapterMock->method('getBucket') + ->willReturn('test'); + $this->adapterMock->method('getClient') + ->willReturn($this->clientMock); + $this->clientMock->method('getObjectUrl') + ->willReturnCallback(function (string $bucket, string $path) { + if ($path === '.') { + $path = ''; + } + + return self::URL . $path; + }); + + $this->driver = new AwsS3( + $this->adapterMock + ); + } + + /** + * @param string|null $basePath + * @param string|null $path + * @param string $expected + * + * @dataProvider getAbsolutePathDataProvider + */ + public function testGetAbsolutePath($basePath, $path, string $expected): void + { + self::assertSame($expected, $this->driver->getAbsolutePath($basePath, $path)); + } + + /** + * @return array + */ + public function getAbsolutePathDataProvider(): array + { + return [ + [ + null, + 'test.png', + self::URL . 'test.png' + ], + [ + self::URL . 'test/test.png', + null, + self::URL . 'test/test.png' + ], + [ + '', + 'test.png', + self::URL . 'test.png' + ], + [ + '', + '/test/test.png', + self::URL . 'test/test.png' + ], + [ + self::URL . 'test/test.png', + self::URL . 'test/test.png', + self::URL . 'test/test.png' + ], + [ + self::URL . 'test/', + 'test.txt', + self::URL . 'test/test.txt' + ], + [ + self::URL . 'media/', + '/catalog/test.png', + self::URL . 'media/catalog/test.png' + ] + ]; + } + + /** + * @param string $basePath + * @param string $path + * @param string $expected + * + * @dataProvider getRelativePathDataProvider + */ + public function testGetRelativePath(string $basePath, string $path, string $expected): void + { + self::assertSame($expected, $this->driver->getRelativePath($basePath, $path)); + } + + /** + * @return array + */ + public function getRelativePathDataProvider(): array + { + return [ + [ + '', + 'test/test.txt', + 'test/test.txt' + ], + [ + '', + '/test/test.txt', + 'test/test.txt' + ], + [ + self::URL, + self::URL . 'test/test.txt', + 'test/test.txt' + ], + + ]; + } + + /** + * @param string $path + * @param string $normalizedPath + * @param bool $has + * @param array $metadata + * @param bool $expected + * @throws FileSystemException + * + * @dataProvider isDirectoryDataProvider + */ + public function testIsDirectory( + string $path, + string $normalizedPath, + bool $has, + array $metadata, + bool $expected + ): void { + $this->adapterMock->method('has') + ->with($normalizedPath) + ->willReturn($has); + $this->adapterMock->method('getMetadata') + ->with($normalizedPath) + ->willReturn($metadata); + + self::assertSame($expected, $this->driver->isDirectory($path)); + } + + /** + * @return array + */ + public function isDirectoryDataProvider(): array + { + return [ + [ + 'some_directory/', + 'some_directory/', + false, + [], + false + ], + [ + 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + true + ], + [ + self::URL . 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + true + ], + [ + self::URL . 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + false + ], + [ + '', + '', + true, + [], + true + ], + [ + '/', + '', + true, + [], + true + ], + ]; + } + + /** + * @param string $path + * @param string $normalizedPath + * @param bool $has + * @param array $metadata + * @param bool $expected + * @throws FileSystemException + * + * @dataProvider isFileDataProvider + */ + public function testIsFile( + string $path, + string $normalizedPath, + bool $has, + array $metadata, + bool $expected + ): void { + $this->adapterMock->method('has') + ->with($normalizedPath) + ->willReturn($has); + $this->adapterMock->method('getMetadata') + ->with($normalizedPath) + ->willReturn($metadata); + + self::assertSame($expected, $this->driver->isFile($path)); + } + + /** + * @return array + */ + public function isFileDataProvider(): array + { + return [ + [ + 'some_file.txt', + 'some_file.txt', + false, + [], + false + ], + [ + 'some_file.txt/', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + true + ], + [ + self::URL . 'some_file.txt', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + true + ], + [ + self::URL . 'some_file.txt/', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + false + ], + [ + '', + '', + false, + [], + false + ], + [ + '/', + '', + false, + [], + false + ] + ]; + } +} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php new file mode 100644 index 0000000000000..cad5ecf314da2 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\FileSystemException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Remote storage configuration enablement. + */ +class RemoteStorageEnableCommand extends Command +{ + private const NAME = 'remote-storage:enable'; + private const ARG_DRIVER = 'driver'; + private const OPTION_BUCKET = 'bucket'; + private const OPTION_REGION = 'region'; + private const OPTION_ACCESS_KEY = 'access-key'; + private const OPTION_SECRET_KEY = 'secret-key'; + private const OPTION_PREFIX = 'prefix'; + private const OPTION_IS_PUBLIC = 'is-public'; + + /** + * @var Writer + */ + private $writer; + + /** + * @param Writer $writer + */ + public function __construct(Writer $writer) + { + $this->writer = $writer; + + parent::__construct(); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Enable remote storage integration') + ->addArgument(self::ARG_DRIVER, InputArgument::REQUIRED, 'Remote driver') + ->addOption(self::OPTION_BUCKET, null, InputOption::VALUE_REQUIRED, 'Bucket') + ->addOption(self::OPTION_REGION, null, InputOption::VALUE_REQUIRED, 'Region') + ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_REQUIRED, 'Access key') + ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_REQUIRED, 'Secret key') + ->addOption(self::OPTION_PREFIX, null, InputOption::VALUE_REQUIRED, 'Prefix', '') + ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_NONE, 'Is public'); + } + + /** + * Executes command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws FileSystemException + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->writer->saveConfig([ + ConfigFilePool::APP_ENV => [ + 'remote_storage' => [ + 'driver' => (string)$input->getArgument(self::ARG_DRIVER), + 'bucket' => (string)$input->getOption(self::OPTION_BUCKET), + 'region' => (string)$input->getOption(self::OPTION_REGION), + 'access_key' => (string)$input->getOption(self::OPTION_ACCESS_KEY), + 'secret_key' => (string)$input->getOption(self::OPTION_SECRET_KEY), + 'prefix' => (string)$input->getOption(self::OPTION_PREFIX), + 'is_public' => (bool)$input->getOption(self::OPTION_IS_PUBLIC) + ] + ] + ], true); + + $output->writeln('<info>Config was saved.</info>'); + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index 11a49147f4f19..4c2c834f0d776 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -7,24 +7,22 @@ namespace Magento\RemoteStorage\Driver; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; use Magento\Framework\Filesystem\DriverPoolInterface; +use Magento\RemoteStorage\Model\Config; /** * The remote driver pool. */ class DriverPool implements DriverPoolInterface { - public const PATH_DRIVER = 'system/file_system/driver'; + public const PATH_DRIVER = 'remote_storage/driver'; + public const PATH_IS_PUBLIC = 'remote_storage/is_public'; public const REMOTE = 'remote'; - /** - * @var ScopeConfigInterface - */ - private $config; - /** * @var DriverPool */ @@ -40,12 +38,17 @@ class DriverPool implements DriverPoolInterface */ private $remotePool; + /** + * @var Config + */ + private $config; + /** * @param BaseDriverPool $driverPool - * @param ScopeConfigInterface $config + * @param Config $config * @param array $remotePool */ - public function __construct(BaseDriverPool $driverPool, ScopeConfigInterface $config, array $remotePool = []) + public function __construct(BaseDriverPool $driverPool, Config $config, array $remotePool = []) { $this->driverPool = $driverPool; $this->config = $config; @@ -53,20 +56,30 @@ public function __construct(BaseDriverPool $driverPool, ScopeConfigInterface $co } /** - * @inheritDoc + * Retrieves remote driver. + * + * @param string $code + * @return DriverInterface + * + * @throws RuntimeException + * @throws FileSystemException */ public function getDriver($code = self::REMOTE): DriverInterface { - $driver = $this->config->getValue('system/file_system/driver'); + if ($code === self::REMOTE) { + if (isset($this->pool[$code])) { + return $this->pool[$code]; + } - if (isset($this->pool[$code])) { - return $this->pool[$code]; - } + $driver = $this->config->getDriver(); + + if ($driver && isset($this->remotePool[$driver])) { + return $this->pool[$code] = $this->remotePool[$driver]->create(); + } - if ($driver && $driver !== BaseDriverPool::FILE) { - return $this->pool[$code] = $this->remotePool[$driver]->create(); + throw new RuntimeException(__('Remote driver is not available.')); } - return $this->pool[$code] = $this->driverPool->getDriver($code); + return $this->driverPool->getDriver($code); } } diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php new file mode 100644 index 0000000000000..1594e53392b76 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage; + +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Filesystem as BaseFilesystem; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; + +/** + * Filesystem implementation for remote storage. + */ +class Filesystem extends BaseFilesystem +{ + /** + * @var bool + */ + private $isEnabled; + + /** + * @var array + */ + private $directoryCodes; + + /** + * @var DriverPool + */ + private $driverPool; + + /** + * @param BaseFilesystem\DirectoryList $directoryList + * @param ReadFactory $readFactory + * @param WriteFactory $writeFactory + * @param Config $config + * @param DriverPool $driverPool + * @param array $directoryCodes + */ + public function __construct( + BaseFilesystem\DirectoryList $directoryList, + ReadFactory $readFactory, + WriteFactory $writeFactory, + Config $config, + DriverPool $driverPool, + array $directoryCodes = [] + ) { + $this->isEnabled = $config->isEnabled(); + $this->driverPool = $driverPool; + $this->directoryCodes = $directoryCodes; + + parent::__construct($directoryList, $readFactory, $writeFactory); + } + + /** + * @inheritDoc + */ + public function getDirectoryRead($directoryCode, $driverCode = DriverPool::REMOTE) + { + $hasCode = !$this->directoryCodes || in_array($directoryCode, $this->directoryCodes, true); + + if ($driverCode === DriverPool::REMOTE && $hasCode && $this->isEnabled) { + $code = $directoryCode . '_' . $driverCode; + + if (!array_key_exists($code, $this->readInstances)) { + $uri = $this->getUri($directoryCode) ?: '/'; + + $this->readInstances[$code] = $this->readFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $uri), + $driverCode + ); + } + + return $this->readInstances[$code]; + } + + return parent::getDirectoryRead($directoryCode); + } + + /** + * @inheritDoc + */ + public function getDirectoryWrite($directoryCode, $driverCode = DriverPool::REMOTE) + { + $hasCode = !$this->directoryCodes || in_array($directoryCode, $this->directoryCodes, true); + + if ($driverCode === DriverPool::REMOTE && $hasCode && $this->isEnabled) { + $code = $directoryCode . '_' . $driverCode; + + if (!array_key_exists($code, $this->writeInstances)) { + $uri = $this->getUri($directoryCode) ?: '/'; + + $this->writeInstances[$code] = $this->writeFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $uri), + $driverCode + ); + } + + return $this->writeInstances[$code]; + } + + return parent::getDirectoryWrite($directoryCode); + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php index b49c647ab6894..164d94cefddee 100644 --- a/app/code/Magento/RemoteStorage/Model/Config.php +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -7,8 +7,10 @@ namespace Magento\RemoteStorage\Model; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\RemoteStorage\Driver\DriverPool; /** * Configuration for remote storage. @@ -16,27 +18,51 @@ class Config { /** - * @var ScopeConfigInterface + * @var DeploymentConfig */ - private $scopeConfig; + private $config; /** - * @param ScopeConfigInterface $scopeConfig + * @param DeploymentConfig $config */ - public function __construct(ScopeConfigInterface $scopeConfig) + public function __construct(DeploymentConfig $config) { - $this->scopeConfig = $scopeConfig; + $this->config = $config; + } + + /** + * Retrieve driver name. + * + * @return string|null + * @throws FileSystemException + * @throws RuntimeException + */ + public function getDriver(): ?string + { + return $this->config->get(DriverPool::PATH_DRIVER, null); } /** * Check if remote FS is enabled. * * @return bool + * @throws FileSystemException + * @throws RuntimeException */ public function isEnabled(): bool { - $driver = $this->scopeConfig->getValue('system/file_system/driver'); + return $this->config->get(DriverPool::PATH_DRIVER) !== null; + } - return $driver && $driver !== DriverPool::FILE; + /** + * Use remote URL for public URLs. + * + * @return bool + * @throws FileSystemException + * @throws RuntimeException + */ + public function isPublic(): bool + { + return (bool)$this->config->get(DriverPool::PATH_IS_PUBLIC, false); } } diff --git a/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php b/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php deleted file mode 100644 index 4972cdda18d9b..0000000000000 --- a/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Model\Config\Source; - -use Magento\Framework\Data\OptionSourceInterface; - -/** - * Provides a list of supported file storages. - */ -class FileStorage implements OptionSourceInterface -{ - /** - * @var array - */ - private $options; - - /** - * @param array $options - */ - public function __construct(array $options = []) - { - $this->options = $options; - } - - /** - * @inheritDoc - */ - public function toOptionArray(): array - { - return $this->options; - } -} diff --git a/app/code/Magento/RemoteStorage/Model/Filesystem.php b/app/code/Magento/RemoteStorage/Model/Filesystem.php deleted file mode 100644 index 040ee005cd57d..0000000000000 --- a/app/code/Magento/RemoteStorage/Model/Filesystem.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Model; - -use Magento\Framework\Filesystem\DirectoryList; -use Magento\Framework\Filesystem\Directory\ReadFactory; -use Magento\Framework\Filesystem\Directory\WriteFactory; - -/** - * Filesystem implementation for remote storage. - */ -class Filesystem extends \Magento\Framework\Filesystem -{ - /** - * @var bool - */ - private $isEnabled; - - /** - * @param DirectoryList $directoryList - * @param ReadFactory $readFactory - * @param WriteFactory $writeFactory - * @param Config $config - */ - public function __construct( - DirectoryList $directoryList, - ReadFactory $readFactory, - WriteFactory $writeFactory, - Config $config - ) { - $this->isEnabled = $config->isEnabled(); - - parent::__construct($directoryList, $readFactory, $writeFactory); - } - - /** - * Gets URL path by code. - * - * @param string $code - * @return string - */ - protected function getDirPath($code): string - { - if ($this->isEnabled) { - return $this->directoryList->getUrlPath($code) ?: '/'; - } - - return parent::getDirPath($code); - } -} diff --git a/app/code/Magento/RemoteStorage/Plugin/Scope.php b/app/code/Magento/RemoteStorage/Plugin/Scope.php new file mode 100644 index 0000000000000..ab723fa1d0c19 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Scope.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Url\ScopeInterface; +use Magento\Framework\UrlInterface; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Filesystem; + +/** + * Modifies the base URL. + */ +class Scope +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @param Config $config + * @param Filesystem $filesystem + */ + public function __construct(Config $config, Filesystem $filesystem) + { + $this->isEnabled = $config->isEnabled() && $config->isPublic(); + $this->filesystem = $filesystem; + } + + /** + * Modifies the base URL. + * + * @param ScopeInterface $subject + * @param string $result + * @param string $type + * @return string + * @throws ValidatorException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetBaseUrl(ScopeInterface $subject, string $result, string $type = ''): string + { + if ($type === UrlInterface::URL_TYPE_MEDIA && $this->isEnabled) { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA, DriverPool::REMOTE) + ->getAbsolutePath(); + } + + return $result; + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php index e84f216ba996c..2e93949b40fce 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php +++ b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php @@ -7,7 +7,9 @@ namespace Magento\RemoteStorage\Plugin; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Filesystem; use Magento\RemoteStorage\Model\Config; use Magento\Sitemap\Model\Sitemap as BaseSitemap; @@ -17,23 +19,23 @@ class Sitemap { /** - * @var Config + * @var Filesystem */ - private $config; + private $filesystem; /** - * @var DriverPool + * @var bool */ - private $driverPool; + private $isEnabled; /** - * @param DriverPool $driverPool + * @param Filesystem $filesystem * @param Config $config */ - public function __construct(DriverPool $driverPool, Config $config) + public function __construct(Filesystem $filesystem, Config $config) { - $this->driverPool = $driverPool; - $this->config = $config; + $this->filesystem = $filesystem; + $this->isEnabled = $config->isEnabled(); } /** @@ -53,10 +55,11 @@ public function afterGetSitemapUrl( string $sitemapPath, string $sitemapFileName ): string { - if ($this->config->isEnabled()) { + if ($this->isEnabled) { $path = trim($sitemapPath . $sitemapFileName, '/'); - return $this->driverPool->getDriver()->getAbsolutePath('', $path); + return $this->filesystem->getDirectoryRead(DirectoryList::ROOT, DriverPool::REMOTE) + ->getAbsolutePath($path); } return $result; diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php deleted file mode 100644 index 1e121c1d1dda1..0000000000000 --- a/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Test\Unit\Model; - -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\RemoteStorage\Driver\DriverPool; -use Magento\RemoteStorage\Model\Config; -use PHPUnit\Framework\TestCase; - -/** - * @see Config - */ -class ConfigTest extends TestCase -{ - /** - * @var Config - */ - private $model; - - /** - * @inheritDoc - */ - protected function setUp(): void - { - $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $configMock->method('getValue') - ->willReturnMap([ - [DriverPool::PATH_DRIVER, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, DriverPool::REMOTE], - ]); - - $this->model = new Config($configMock); - } - - public function testIsEnabled(): void - { - self::assertTrue($this->model->isEnabled()); - } -} diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml deleted file mode 100644 index 1437f0636ac03..0000000000000 --- a/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml +++ /dev/null @@ -1,19 +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:ObjectManager/etc/config.xsd"> - <type name="Magento\RemoteStorage\Model\Config\Source\FileStorage"> - <arguments> - <argument name="options" xsi:type="array"> - <item name="file" xsi:type="array"> - <item name="value" xsi:type="const">Magento\Framework\Filesystem\DriverPool::FILE</item> - <item name="label" xsi:type="string" translate="true">File System</item> - </item> - </argument> - </arguments> - </type> -</config> diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml deleted file mode 100644 index aa5865c099dc5..0000000000000 --- a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml +++ /dev/null @@ -1,20 +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:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="system"> - <group id="file_system" translate="label" type="text" sortOrder="850" showInDefault="1"> - <label>Storage Configuration</label> - <field id="driver" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>General Storage</label> - <source_model>Magento\RemoteStorage\Model\Config\Source\FileStorage</source_model> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 9bc960691d034..a43a6160c554f 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -16,36 +16,56 @@ <argument name="driverPool" xsi:type="object">Magento\RemoteStorage\Driver\DriverPool</argument> </arguments> </virtualType> - <virtualType name="remoteFilesystem" type="Magento\RemoteStorage\Model\Filesystem"> + <type name="Magento\RemoteStorage\Filesystem"> <arguments> <argument name="writeFactory" xsi:type="object">remoteWriteFactory</argument> <argument name="readFactory" xsi:type="object">remoteReadFactory</argument> </arguments> + </type> + <virtualType name="customRemoteFilesystem" type="Magento\RemoteStorage\Filesystem"> + <arguments> + <argument name="directoryCodes" xsi:type="array"> + <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> + </argument> + </arguments> </virtualType> + <virtualType name="fullRemoteFilesystem" type="Magento\RemoteStorage\Filesystem" /> + <preference for="Magento\Framework\Filesystem" type="customRemoteFilesystem"/> + <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + <argument name="driverCode" xsi:type="const">Magento\RemoteStorage\Driver\DriverPool::REMOTE</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Model\Sitemap"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> + </type> <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Save"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> <type name="Magento\Sitemap\Block\Adminhtml\Grid\Renderer\Link"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> - <type name="Magento\Sitemap\Model\Sitemap"> - <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> - </arguments> - <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> + <type name="Magento\Framework\Url\ScopeInterface"> + <plugin name="remote_url" type="Magento\RemoteStorage\Plugin\Scope" /> </type> - <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> + <type name="Magento\Framework\Console\CommandListInterface"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="commands" xsi:type="array"> + <item name="remoteStorageEnable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageEnableCommand</item> + </argument> </arguments> </type> </config> diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml index cc9f2e7328292..6c1b7f0b05a34 100644 --- a/app/code/Magento/RemoteStorage/etc/module.xml +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Backend"/> <module name="Magento_Sitemap"/> + <module name="Magento_Store"/> </sequence> </module> </config> diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index 143889364781f..3ef113fa63fa7 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -160,7 +160,7 @@ public function afterLoad() 'size' => is_array($stat) ? $stat['size'] : 0, //phpcs:ignore Magento2.Functions.DiscouragedFunction 'name' => basename($value), - 'type' => $this->getMimeType($fileName), + 'type' => $stat['mimetype'] ?? $this->getMimeType($fileName), 'exists' => true, ] ]; diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index b5929f8ce7a08..b944ceb94628b 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -311,7 +311,10 @@ private function validateDestination(string $destinationFolder): void { if ($this->_allowCreateFolders) { $this->createDestinationFolder($destinationFolder); - } elseif (!$this->getFileDriver()->isWritable($destinationFolder)) { + } elseif (!$this->getTargetDirectory() + ->getDirectoryWrite(DirectoryList::ROOT) + ->isWritable($destinationFolder) + ) { throw new FileSystemException(__('Destination folder is not writable or does not exists.')); } } @@ -334,13 +337,19 @@ protected function chmod($file) * * @param string $tmpPath * @param string $destPath - * @return bool|void + * @return bool + * @throws FileSystemException */ protected function _moveFile($tmpPath, $destPath) { - $rootPath = $this->getDocumentRoot()->getPath(); - $destPath = str_replace($this->getDirectoryList()->getPath($rootPath), '', $destPath); - $directory = $this->getTargetDirectory()->getDirectoryWrite($rootPath); + $rootCode = $this->getDocumentRoot()->getPath(); + + if (strpos($destPath, $this->getDirectoryList()->getPath($rootCode)) !== 0) { + $rootCode = DirectoryList::ROOT; + } + + $destPath = str_replace($this->getDirectoryList()->getPath($rootCode), '', $destPath); + $directory = $this->getTargetDirectory()->getDirectoryWrite($rootCode); return $this->getFileDriver()->rename( $tmpPath, @@ -745,8 +754,10 @@ private function createDestinationFolder(string $destinationFolder) $destinationFolder = substr($destinationFolder, 0, -1); } - if (!$this->getFileDriver()->isDirectory($destinationFolder)) { - $result = $this->getFileDriver()->createDirectory($destinationFolder); + $rootDirectory = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + + if (!$rootDirectory->isDirectory($destinationFolder)) { + $result = $rootDirectory->getDriver()->createDirectory($destinationFolder); if (!$result) { throw new FileSystemException(__('Unable to create directory %1.', $destinationFolder)); } From 00a03e958a24a2d7872b4510be8544acef6af813 Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Sat, 3 Oct 2020 10:03:32 +0200 Subject: [PATCH 055/195] Fixes after CR --- .../Config/QueueConfigItem/DataMapperTest.php | 33 ++++++++++--------- .../Config/QueueConfigItem/DataMapper.php | 12 +++---- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index 5870e7cd80e67..62581ad13a84b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -6,7 +6,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Framework\MessageQueue\Test\Unit\Topology\Config\QueueConfigItem; @@ -23,17 +22,17 @@ class DataMapperTest extends TestCase /** * @var Data|MockObject */ - private $configData; + private $configDataMock; /** * @var CommunicationConfig|MockObject */ - private $communicationConfig; + private $communicationConfigMock; /** * @var ResponseQueueNameBuilder|MockObject */ - private $queueNameBuilder; + private $queueNameBuilderMock; /** * @var DataMapper @@ -45,10 +44,14 @@ class DataMapperTest extends TestCase */ protected function setUp(): void { - $this->configData = $this->createMock(Data::class); - $this->communicationConfig = $this->createMock(CommunicationConfig::class); - $this->queueNameBuilder = $this->createMock(ResponseQueueNameBuilder::class); - $this->model = new DataMapper($this->configData, $this->communicationConfig, $this->queueNameBuilder); + $this->configDataMock = $this->createMock(Data::class); + $this->communicationConfigMock = $this->createMock(CommunicationConfig::class); + $this->queueNameBuilderMock = $this->createMock(ResponseQueueNameBuilder::class); + $this->model = new DataMapper( + $this->configDataMock, + $this->communicationConfigMock, + $this->queueNameBuilderMock + ); } /** @@ -112,11 +115,11 @@ public function testGetMappedData(): void ['topic02', ['name' => 'topic02', 'is_synchronous' => false]], ]; - $this->communicationConfig->expects($this->exactly(2)) + $this->communicationConfigMock->expects($this->exactly(2)) ->method('getTopic') ->willReturnMap($communicationMap); - $this->configData->expects($this->once())->method('get')->willReturn($data); - $this->queueNameBuilder->expects($this->once()) + $this->configDataMock->expects($this->once())->method('get')->willReturn($data); + $this->queueNameBuilderMock->expects($this->once()) ->method('getQueueName') ->with('topic01') ->willReturn('responseQueue.topic01'); @@ -218,15 +221,15 @@ public function testGetMappedDataForWildcard(): void 'topic08.part2.some.test' => ['name' => 'topic08.part2.some.test', 'is_synchronous' => true], ]; - $this->communicationConfig->expects($this->once()) + $this->communicationConfigMock->expects($this->once()) ->method('getTopic') ->with('topic01') ->willReturn(['name' => 'topic01', 'is_synchronous' => true]); - $this->communicationConfig->expects($this->any()) + $this->communicationConfigMock->expects($this->any()) ->method('getTopics') ->willReturn($communicationData); - $this->configData->expects($this->once())->method('get')->willReturn($data); - $this->queueNameBuilder->expects($this->any()) + $this->configDataMock->expects($this->once())->method('get')->willReturn($data); + $this->queueNameBuilderMock->expects($this->any()) ->method('getQueueName') ->willReturnCallback(function ($value) { return 'responseQueue.' . $value; diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index 627dca68d14a4..d48fa637fd885 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -73,12 +73,12 @@ public function getMappedData(): array foreach ($exchange['bindings'] as $binding) { if ($binding['destinationType'] === 'queue') { $queueItems = $this->createQueueItems( - (string) $binding['destination'], - (string) $binding['topic'], - (array) $binding['arguments'], - (string) $connection + (string)$binding['destination'], + (string)$binding['topic'], + (array)$binding['arguments'], + (string)$connection ); - $this->mappedData += $queueItems; + $this->mappedData = array_merge($this->mappedData, $queueItems); } } } @@ -141,7 +141,7 @@ private function isSynchronousTopic(string $topicName): bool { try { $topic = $this->communicationConfig->getTopic($topicName); - return (bool) $topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + return (bool)$topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; } catch (LocalizedException $exception) { throw new LocalizedException(new Phrase('Error while checking if topic is synchronous')); } From 67b9b6a5d723e95ed31d9d1be8658fd44daffd69 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 6 Oct 2020 09:35:09 +0300 Subject: [PATCH 056/195] MC-37718: Grouped product remains In Stock On Mass Update --- .../Plugin/MassUpdateProductAttribute.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index ee5f16989ba37..2dd47eae16959 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -58,7 +58,7 @@ class MassUpdateProductAttribute /** * @var ParentItemProcessorInterface[] */ - private $parentItemProcessors; + private $parentItemProcessorPool; /** * @var ProductRepositoryInterface @@ -73,7 +73,7 @@ class MassUpdateProductAttribute * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param ProductRepositoryInterface $productRepository - * @param ParentItemProcessorInterface[] $parentItemProcessors + * @param ParentItemProcessorInterface[] $parentItemProcessorPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -85,7 +85,7 @@ public function __construct( \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, \Magento\Framework\Message\ManagerInterface $messageManager, ProductRepositoryInterface $productRepository, - array $parentItemProcessors = [] + array $parentItemProcessorPool = [] ) { $this->stockIndexerProcessor = $stockIndexerProcessor; $this->dataObjectHelper = $dataObjectHelper; @@ -95,7 +95,7 @@ public function __construct( $this->attributeHelper = $attributeHelper; $this->messageManager = $messageManager; $this->productRepository = $productRepository; - $this->parentItemProcessors = $parentItemProcessors; + $this->parentItemProcessorPool = $parentItemProcessorPool; } /** @@ -188,7 +188,7 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa */ private function processParents(ProductInterface $product): void { - foreach ($this->parentItemProcessors as $processor) { + foreach ($this->parentItemProcessorPool as $processor) { $processor->process($product); } } From d8023289262ecadfd3e2dbbe6ac3b5d012f5123a Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Mon, 5 Oct 2020 23:07:07 +0200 Subject: [PATCH 057/195] Another fix after CR --- .../Config/QueueConfigItem/DataMapperTest.php | 4 ++-- .../Topology/Config/QueueConfigItem/DataMapper.php | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index 62581ad13a84b..46ea82b887db6 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -1,12 +1,12 @@ <?php -declare(strict_types=1); - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\MessageQueue\Test\Unit\Topology\Config\QueueConfigItem; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index d48fa637fd885..912aa4a6b0fb1 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -1,11 +1,12 @@ <?php -declare(strict_types=1); - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\MessageQueue\Topology\Config\QueueConfigItem; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; @@ -67,7 +68,7 @@ public function __construct( public function getMappedData(): array { if (null === $this->mappedData) { - $this->mappedData = []; + $mappedData = []; foreach ($this->configData->get() as $exchange) { $connection = $exchange['connection']; foreach ($exchange['bindings'] as $binding) { @@ -78,10 +79,11 @@ public function getMappedData(): array (array)$binding['arguments'], (string)$connection ); - $this->mappedData = array_merge($this->mappedData, $queueItems); + $mappedData[] = $queueItems; } } } + $this->mappedData = array_merge([], ...$mappedData); } return $this->mappedData; @@ -158,7 +160,7 @@ private function matchSynchronousTopics(string $wildcard): array $topicDefinitions = array_filter( $this->communicationConfig->getTopics(), function ($item) { - return (bool) $item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + return (bool)$item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; } ); From bebc574b270bb6dc1d49217ab99898b0757bd0fb Mon Sep 17 00:00:00 2001 From: Dmitry Tsymbal <d.tsymbal@atwix.com> Date: Wed, 7 Oct 2020 13:50:30 +0300 Subject: [PATCH 058/195] Admin Delete CMS Block Test --- ...AdminDeleteCMSBlockFromGridActionGroup.xml | 21 +++++++++ .../AdminOpenCMSBlocksGridActionGroup.xml | 19 ++++++++ ...hCMSBlockInGridByIdentifierActionGroup.xml | 20 ++++++++ ...ertAdminCMSBlockIsNotInGridActionGroup.xml | 17 +++++++ .../Mftf/Section/AdminBlockGridSection.xml | 1 + .../Mftf/Section/BlockPageActionsSection.xml | 2 + .../Mftf/Test/AdminDeleteCmsBlockTest.xml | 47 +++++++++++++++++++ 7 files changed, 127 insertions(+) create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml new file mode 100644 index 0000000000000..a61f565bac2bc --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.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="AdminDeleteCMSBlockFromGridActionGroup"> + <arguments> + <argument name="identifier" type="entity"/> + </arguments> + <click selector="{{BlockPageActionsSection.select(identifier)}}" stepKey="clickSelect"/> + <click selector="{{BlockPageActionsSection.delete(identifier)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{BlockPageActionsSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{BlockPageActionsSection.deleteConfirm}}" stepKey="clickOkButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml new file mode 100644 index 0000000000000..18e7e5fb52615 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenCMSBlocksGridActionGroup"> + <annotations> + <description>Navigate to the Admin Blocks Grid page.</description> + </annotations> + + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml new file mode 100644 index 0000000000000..1099cd7e753c9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSearchCMSBlockInGridByIdentifierActionGroup"> + <arguments> + <argument name="identifier" type="string"/> + </arguments> + <click selector="{{BlockPageActionsSection.FilterBtn}}" stepKey="clickFilterButton"/> + <fillField selector="{{BlockPageActionsSection.URLKey}}" userInput="{{identifier}}" stepKey="fillIdentifierField"/> + <click selector="{{BlockPageActionsSection.ApplyFiltersBtn}}" stepKey="clickApplyFiltersButton"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml new file mode 100644 index 0000000000000..1b5a5301eda1b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.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="AssertAdminCMSBlockIsNotInGridActionGroup"> + <arguments> + <argument name="identifier" type="entity"/> + </arguments> + <dontSee userInput="{{identifier}}" selector="{{AdminBlockGridSection.gridDataRow}}" stepKey="dontSeeCmsBlockInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml index ab15570a01f40..a9c9a5943529c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -14,5 +14,6 @@ <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> + <element name="gridDataRow" type="input" selector=".data-row .data-grid-cell-content"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index ac9c66fe82c74..529000dc44c3a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -20,5 +20,7 @@ <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> <element name="blockGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> + <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml new file mode 100644 index 0000000000000..4274973796b64 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml @@ -0,0 +1,47 @@ +<?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="AdminDeleteCmsBlockTest"> + <annotations> + <features value="Cms"/> + <stories value="CMS Blocks Deleting"/> + <title value="Admin should be able to delete CMS block from grid"/> + <description value="Admin should be able to delete CMS block from grid"/> + <group value="Cms"/> + <severity value="MINOR"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="createCMSBlock"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenCMSBlocksGridActionGroup" stepKey="navigateToCmsBlocksGrid"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFilters"/> + <actionGroup ref="AdminSearchCMSBlockInGridByIdentifierActionGroup" stepKey="findCreatedCmsBlock"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AdminDeleteCMSBlockFromGridActionGroup" stepKey="deleteCmsBlockFromGrid"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="You deleted the block."/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFiltersAfterBlockDeleting"/> + <actionGroup ref="AdminSearchCMSBlockInGridByIdentifierActionGroup" stepKey="searchDeletedCmsBlock"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCMSBlockIsNotInGridActionGroup" stepKey="assertDeletedCMSBlockIsNotInGrid"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + </test> +</tests> From faa8fb5e4aaefc474a6b8a348bca99787309c9aa Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 7 Oct 2020 12:06:23 -0500 Subject: [PATCH 059/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Indexer.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 2821a46f29416..5a3a9faf3e4f3 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -250,10 +250,8 @@ public function getView() */ public function getState() { - if (!$this->state) { - $this->state = $this->stateFactory->create(); - $this->state->loadByIndexer($this->getId()); - } + $this->state = $this->stateFactory->create(); + $this->state->loadByIndexer($this->getId()); return $this->state; } From d0b9f8ee04c2f7f9d0d8562285f505834292ecf6 Mon Sep 17 00:00:00 2001 From: Sagar Dahiwala <sagar.dahiwala@briteskies.com> Date: Wed, 7 Oct 2020 16:19:31 -0400 Subject: [PATCH 060/195] magento/partners-magento2b2b#325: Automate Currency availability test for Company Credit - Added data fixture for base currecy of second website --- ...cond_website_with_base_second_currency.php | 47 +++++++++++++++++++ ...ite_with_base_second_currency_rollback.php | 27 +++++++++++ 2 files changed, 74 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php create mode 100644 dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php new file mode 100644 index 0000000000000..abe19edfbf148 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + 'EUR', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId +); +$configResource->saveConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE, + 'default', + 0 +); + +/** + * Configuration cache clean is required to reload currency setting + */ +/** @var Magento\Config\App\Config\Type\System $config */ +$config = $objectManager->get(\Magento\Config\App\Config\Type\System::class); +$config->clean(); + +$observer = $objectManager->get(\Magento\Framework\Event\Observer::class); +$objectManager->get(\Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange::class) + ->execute($observer); + +/** @var \Magento\Directory\Model\ResourceModel\Currency $rate */ +$rate = $objectManager->create(\Magento\Directory\Model\ResourceModel\Currency::class); +$rate->saveRates([ + 'USD' => ['EUR' => 2], + 'EUR' => ['USD' => 0.5] +]); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php new file mode 100644 index 0000000000000..b1927dabb1189 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->deleteConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + 'default', + 0 +); +$website = $objectManager->create(\Magento\Store\Model\Website::class); +/** @var $website \Magento\Store\Model\Website */ +$websiteId = $website->load('test', 'code')->getId(); +if ($websiteId) { + $configResource->deleteConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId + ); +} + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); From 19b4fdb237c9a3817ad36aefd92d451b1cd0cdd1 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 7 Oct 2020 16:08:59 -0500 Subject: [PATCH 061/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Indexer.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 5a3a9faf3e4f3..47a0b3205bc63 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -250,8 +250,10 @@ public function getView() */ public function getState() { - $this->state = $this->stateFactory->create(); - $this->state->loadByIndexer($this->getId()); + if (!$this->state) { + $this->state = $this->stateFactory->create(); + $this->state->loadByIndexer($this->getId()); + } return $this->state; } @@ -320,7 +322,10 @@ public function isInvalid() */ public function isWorking() { - return $this->getState()->getStatus() == StateInterface::STATUS_WORKING; + //retrieve actual state, not cached one + $state = $this->stateFactory->create(); + $state->loadByIndexer($this->getId()); + return $state->getStatus() == StateInterface::STATUS_WORKING; } /** From 28a447ade71487ae7a3da104fa1beebd782de891 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 7 Oct 2020 17:04:16 -0500 Subject: [PATCH 062/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Processor.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 534ea805bb8fc..107cf24aabeab 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -72,12 +72,14 @@ public function reindexAllInvalid() if (!in_array($sharedIndex, $sharedIndexesComplete)) { $indexer->reindexAll(); } else { - /** @var \Magento\Indexer\Model\Indexer\State $state */ - $state = $indexer->getState(); - $state->setStatus(StateInterface::STATUS_WORKING); - $state->save(); - $state->setStatus(StateInterface::STATUS_VALID); - $state->save(); + if (!$indexer->isWorking()) { + /** @var \Magento\Indexer\Model\Indexer\State $state */ + $state = $indexer->getState(); + $state->setStatus(StateInterface::STATUS_WORKING); + $state->save(); + $state->setStatus(StateInterface::STATUS_VALID); + $state->save(); + } } if ($sharedIndex) { $sharedIndexesComplete[] = $sharedIndex; From 7e2098d812b2e733759792662892e722dadfe82e Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 10:54:52 -0500 Subject: [PATCH 063/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Category/Product/Action/Rows.php | 14 +++++- app/code/Magento/Indexer/Model/Indexer.php | 5 +-- app/code/Magento/Indexer/Model/Processor.php | 12 ++++- .../Indexer/Model/WorkingStateProvider.php | 45 +++++++++++++++++++ 4 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 app/code/Magento/Indexer/Model/WorkingStateProvider.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index 5d81c1405efe0..d3d6c8b97aac5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -17,6 +17,7 @@ use Magento\Catalog\Model\Category; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Indexer\Model\WorkingStateProvider; /** * Reindex multiple rows action. @@ -48,6 +49,11 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private $indexerRegistry; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + /** * @param ResourceConnection $resource * @param StoreManagerInterface $storeManager @@ -57,6 +63,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry + * @param WorkingStateProvider|null $workingStateProvider */ public function __construct( ResourceConnection $resource, @@ -66,12 +73,15 @@ public function __construct( MetadataPool $metadataPool = null, CacheContext $cacheContext = null, EventManagerInterface $eventManager = null, - IndexerRegistry $indexerRegistry = null + IndexerRegistry $indexerRegistry = null, + WorkingStateProvider $workingStateProvider = null ) { parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); + $this->workingStateProvider = $workingStateProvider ?: + ObjectManager::getInstance()->get(WorkingStateProvider::class); } /** @@ -97,7 +107,7 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByCategories = array_unique($this->limitationByCategories); $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); - $workingState = $indexer->isWorking(); + $workingState = $this->workingStateProvider->isWorking($indexer->getId()); if ($useTempTable && !$workingState && $indexer->isScheduled()) { foreach ($this->storeManager->getStores() as $store) { diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 47a0b3205bc63..2821a46f29416 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -322,10 +322,7 @@ public function isInvalid() */ public function isWorking() { - //retrieve actual state, not cached one - $state = $this->stateFactory->create(); - $state->loadByIndexer($this->getId()); - return $state->getStatus() == StateInterface::STATUS_WORKING; + return $this->getState()->getStatus() == StateInterface::STATUS_WORKING; } /** diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 107cf24aabeab..a2f200f31d7a6 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -35,22 +35,30 @@ class Processor */ protected $mviewProcessor; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + /** * @param ConfigInterface $config * @param IndexerInterfaceFactory $indexerFactory * @param Indexer\CollectionFactory $indexersFactory * @param \Magento\Framework\Mview\ProcessorInterface $mviewProcessor + * @param WorkingStateProvider $workingStateProvider */ public function __construct( ConfigInterface $config, IndexerInterfaceFactory $indexerFactory, Indexer\CollectionFactory $indexersFactory, - \Magento\Framework\Mview\ProcessorInterface $mviewProcessor + \Magento\Framework\Mview\ProcessorInterface $mviewProcessor, + WorkingStateProvider $workingStateProvider ) { $this->config = $config; $this->indexerFactory = $indexerFactory; $this->indexersFactory = $indexersFactory; $this->mviewProcessor = $mviewProcessor; + $this->workingStateProvider = $workingStateProvider; } /** @@ -72,7 +80,7 @@ public function reindexAllInvalid() if (!in_array($sharedIndex, $sharedIndexesComplete)) { $indexer->reindexAll(); } else { - if (!$indexer->isWorking()) { + if (!$this->workingStateProvider->isWorking($indexer->getId())) { /** @var \Magento\Indexer\Model\Indexer\State $state */ $state = $indexer->getState(); $state->setStatus(StateInterface::STATUS_WORKING); diff --git a/app/code/Magento/Indexer/Model/WorkingStateProvider.php b/app/code/Magento/Indexer/Model/WorkingStateProvider.php new file mode 100644 index 0000000000000..d77c1b67ecfd7 --- /dev/null +++ b/app/code/Magento/Indexer/Model/WorkingStateProvider.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Indexer\Model; + +use Magento\Indexer\Model\Indexer\StateFactory; +use Magento\Framework\Indexer\StateInterface; + +/** + * Provide actual working status of the indexer + */ +class WorkingStateProvider +{ + /** + * @var StateFactory + */ + private $stateFactory; + + /** + * @param StateFactory $stateFactory + */ + public function __construct( + StateFactory $stateFactory + ) { + $this->stateFactory = $stateFactory; + } + + /** + * Execute user functions + * + * @param string $indexerId + * @return bool + */ + public function isWorking(string $indexerId) : bool + { + $state = $this->stateFactory->create(); + $state->loadByIndexer($indexerId); + + return $state->getStatus() === StateInterface::STATUS_WORKING; + } +} From 09a2ae3a69c010eda0358f4eb603532510c3d4ce Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 11:06:57 -0500 Subject: [PATCH 064/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Indexer.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 2821a46f29416..8a922c6be2ef8 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -61,13 +61,19 @@ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface */ protected $indexersFactory; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + /** * @param ConfigInterface $config * @param ActionFactory $actionFactory * @param StructureFactory $structureFactory * @param \Magento\Framework\Mview\ViewInterface $view * @param Indexer\StateFactory $stateFactory - * @param Indexer\CollectionFactory $indexersFactory + * @param Indexer\CollectionFactory $indexersFactory, + * @param WorkingStateProvider $workingStateProvider, * @param array $data */ public function __construct( @@ -77,6 +83,7 @@ public function __construct( \Magento\Framework\Mview\ViewInterface $view, Indexer\StateFactory $stateFactory, Indexer\CollectionFactory $indexersFactory, + WorkingStateProvider $workingStateProvider, array $data = [] ) { $this->config = $config; @@ -85,6 +92,7 @@ public function __construct( $this->view = $view; $this->stateFactory = $stateFactory; $this->indexersFactory = $indexersFactory; + $this->workingStateProvider = $workingStateProvider; parent::__construct($data); } @@ -405,7 +413,7 @@ protected function getStructureInstance() */ public function reindexAll() { - if ($this->getState()->getStatus() != StateInterface::STATUS_WORKING) { + if (!$this->workingStateProvider->isWorking($this->getId())) { $state = $this->getState(); $state->setStatus(StateInterface::STATUS_WORKING); $state->save(); From 646fcd474157906c682c9feb6312d84a5f1d9c4b Mon Sep 17 00:00:00 2001 From: Sagar Dahiwala <sagar.dahiwala@briteskies.com> Date: Thu, 8 Oct 2020 12:24:09 -0400 Subject: [PATCH 065/195] magento/partners-magento2b2b#325: Automate Currency availability test for Company Credit - Removed additional rates from the data fixture. --- .../_files/second_website_with_base_second_currency.php | 7 ------- .../second_website_with_base_second_currency_rollback.php | 3 ++- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php index abe19edfbf148..3dc610c5fb943 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php @@ -38,10 +38,3 @@ $observer = $objectManager->get(\Magento\Framework\Event\Observer::class); $objectManager->get(\Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange::class) ->execute($observer); - -/** @var \Magento\Directory\Model\ResourceModel\Currency $rate */ -$rate = $objectManager->create(\Magento\Directory\Model\ResourceModel\Currency::class); -$rate->saveRates([ - 'USD' => ['EUR' => 2], - 'EUR' => ['USD' => 0.5] -]); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php index b1927dabb1189..4fac07ae4f51f 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php @@ -3,9 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Config\Model\ResourceModel\Config $configResource */ $configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); $configResource->deleteConfig( From 0c1afb899a52c244961b3c9e36270817ce8459d3 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 16:36:22 -0500 Subject: [PATCH 066/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Indexer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 8a922c6be2ef8..2ca08fb35cefd 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -72,8 +72,8 @@ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface * @param StructureFactory $structureFactory * @param \Magento\Framework\Mview\ViewInterface $view * @param Indexer\StateFactory $stateFactory - * @param Indexer\CollectionFactory $indexersFactory, - * @param WorkingStateProvider $workingStateProvider, + * @param Indexer\CollectionFactory $indexersFactory + * @param WorkingStateProvider $workingStateProvider * @param array $data */ public function __construct( From 1fd168b9955e4ad92566e6a8046d75a38ca11c63 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 16:36:56 -0500 Subject: [PATCH 067/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Catalog/Model/Indexer/Category/Product/Action/Rows.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index d3d6c8b97aac5..bd967b539877d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -22,7 +22,6 @@ /** * Reindex multiple rows action. * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction From 88d6037b4e830a6bec6b58ce0e103ab429bb0113 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 17:57:29 -0500 Subject: [PATCH 068/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Category/Product/Action/Rows.php | 9 +- .../Category/Product/Action/RowsTest.php | 241 ++++++++++++++++++ 2 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index bd967b539877d..3ff7b519ac132 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -13,10 +13,11 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Category; -use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Indexer\Model\WorkingStateProvider; /** @@ -59,6 +60,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param Config $config * @param QueryGenerator|null $queryGenerator * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry @@ -70,12 +72,13 @@ public function __construct( Config $config, QueryGenerator $queryGenerator = null, MetadataPool $metadataPool = null, + ?TableMaintainer $tableMaintainer = null, CacheContext $cacheContext = null, EventManagerInterface $eventManager = null, IndexerRegistry $indexerRegistry = null, - WorkingStateProvider $workingStateProvider = null + ?WorkingStateProvider $workingStateProvider = null ) { - parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool, $tableMaintainer); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php new file mode 100644 index 0000000000000..8ec2d3fd1afe7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php @@ -0,0 +1,241 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Action; + +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\Action\Rows; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Indexer\Model\WorkingStateProvider; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Indexer\IndexerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for Rows action + */ +class RowsTest extends TestCase +{ + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + + /** + * @var ResourceConnection|MockObject + */ + private $resource; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Config|MockObject + */ + private $config; + + /** + * @var QueryGenerator|MockObject + */ + private $queryGenerator; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var CacheContext|MockObject + */ + private $cacheContext; + + /** + * @var EventManagerInterface|MockObject + */ + private $eventManager; + + /** + * @var IndexerRegistry|MockObject + */ + private $indexerRegistry; + + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainer; + + /** + * @var IndexerInterface|MockObject + */ + private $indexer; + + /** + * @var AdapterInterface|MockObject + */ + private $connection; + + /** + * @var Select|MockObject + */ + private $select; + + /** + * @var Rows + */ + private $rowsModel; + + protected function setUp() : void + { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resource = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->resource->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connection); + $this->select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->select->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinLeft') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('columns') + ->willReturnSelf(); + $this->connection->expects($this->any()) + ->method('select') + ->willReturn($this->select); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->queryGenerator = $this->getMockBuilder(QueryGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cacheContext = $this->getMockBuilder(CacheContext::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eventManager = $this->getMockBuilder(EventManagerInterface::class) + ->getMockForAbstractClass(); + $this->indexerRegistry = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexer = $this->getMockBuilder(IndexerInterface::class) + ->getMockForAbstractClass(); + $this->tableMaintainer = $this->getMockBuilder(TableMaintainer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->rowsModel = new Rows( + $this->resource, + $this->storeManager, + $this->config, + $this->queryGenerator, + $this->metadataPool, + $this->tableMaintainer, + $this->cacheContext, + $this->eventManager, + $this->indexerRegistry, + $this->workingStateProvider + ); + } + + public function testExecuteWithIndexerWorking() : void + { + $categoryId = '1'; + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any()) + ->method('getRootCategoryId') + ->willReturn($categoryId); + $store->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $attribute = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->config->expects($this->any()) + ->method('getAttribute') + ->willReturn($attribute); + + $table = $this->getMockBuilder(Table::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any()) + ->method('newTable') + ->willReturn($table); + + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $this->connection->expects($this->any()) + ->method('fetchAll') + ->willReturn([]); + + $this->connection->expects($this->any()) + ->method('fetchOne') + ->willReturn($categoryId); + $this->indexerRegistry->expects($this->any()) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexer->expects($this->any()) + ->method('getId') + ->willReturn(ProductCategoryIndexer::INDEXER_ID); + $this->workingStateProvider->expects($this->any()) + ->method('isWorking') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn(true); + $this->storeManager->expects($this->any()) + ->method('getStores') + ->willReturn([$store]); + + $this->connection->expects($this->once()) + ->method('delete'); + + $result = $this->rowsModel->execute([1, 2, 3]); + $this->assertInstanceOf(Rows::class, $result); + } +} From a9d19143e5158332a7b5d6f2b7735864e29fbb84 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 18:01:52 -0500 Subject: [PATCH 069/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Test/Unit/Model/IndexerTest.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 662856e2187d5..9b2dc52e2f247 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -17,6 +17,7 @@ use Magento\Indexer\Model\Indexer\CollectionFactory; use Magento\Indexer\Model\Indexer\State; use Magento\Indexer\Model\Indexer\StateFactory; +use Magento\Indexer\Model\WorkingStateProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -55,8 +56,16 @@ class IndexerTest extends TestCase */ protected $indexFactoryMock; + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + protected function setUp(): void { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); $this->configMock = $this->getMockForAbstractClass( ConfigInterface::class, [], @@ -99,7 +108,8 @@ protected function setUp(): void $structureFactory, $this->viewMock, $this->stateFactoryMock, - $this->indexFactoryMock + $this->indexFactoryMock, + $this->workingStateProvider ); } @@ -211,7 +221,7 @@ public function testReindexAll() $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); - $stateMock->expects($this->once())->method('getStatus')->willReturn('idle'); + $stateMock->expects($this->any())->method('getStatus')->willReturn('idle'); $stateMock->expects($this->exactly(2))->method('save')->willReturnSelf(); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); @@ -251,7 +261,7 @@ public function testReindexAllWithException() $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); - $stateMock->expects($this->once())->method('getStatus')->willReturn('idle'); + $stateMock->expects($this->any())->method('getStatus')->willReturn('idle'); $stateMock->expects($this->exactly(2))->method('save')->willReturnSelf(); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); @@ -296,7 +306,7 @@ public function testReindexAllWithError() $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); - $stateMock->expects($this->once())->method('getStatus')->willReturn('idle'); + $stateMock->expects($this->any())->method('getStatus')->willReturn('idle'); $stateMock->expects($this->exactly(2))->method('save')->willReturnSelf(); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); From 7252a06b6134fd6b5cc8327d5648574ce0b8b639 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 8 Oct 2020 18:04:00 -0500 Subject: [PATCH 070/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Test/Unit/Model/ProcessorTest.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index 7a06fb745ba89..11cce22377e9f 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -15,6 +15,7 @@ use Magento\Indexer\Model\Indexer\Collection; use Magento\Indexer\Model\Indexer\CollectionFactory; use Magento\Indexer\Model\Indexer\State; +use Magento\Indexer\Model\WorkingStateProvider; use Magento\Indexer\Model\Processor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -46,8 +47,16 @@ class ProcessorTest extends TestCase */ protected $viewProcessorMock; + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + protected function setUp(): void { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); $this->configMock = $this->getMockForAbstractClass( ConfigInterface::class, [], @@ -75,7 +84,8 @@ protected function setUp(): void $this->configMock, $this->indexerFactoryMock, $this->indexersFactoryMock, - $this->viewProcessorMock + $this->viewProcessorMock, + $this->workingStateProvider ); } From c2758f41ba82574273ce9a47786a1ba57eb1eeef Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 9 Oct 2020 12:15:45 +0300 Subject: [PATCH 071/195] MC-35740: Using API to capture payment --- .../AddTransactionCommentAfterCapture.php | 63 +++++++++++++++ .../AddTransactionCommentAfterCaptureTest.php | 81 +++++++++++++++++++ app/code/Magento/Sales/etc/webapi_rest/di.xml | 3 + app/code/Magento/Sales/etc/webapi_soap/di.xml | 3 + 4 files changed, 150 insertions(+) create mode 100644 app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php create mode 100644 app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php diff --git a/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php b/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php new file mode 100644 index 0000000000000..256d097b9eef0 --- /dev/null +++ b/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Plugin\Model\Service\Invoice; + +use Magento\Framework\DB\TransactionFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Model\Service\InvoiceService; + +/** + * Plugin to add transaction comment after capture invoice + */ +class AddTransactionCommentAfterCapture +{ + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var TransactionFactory + */ + private $transactionFactory; + + /** + * @param InvoiceRepositoryInterface $invoiceRepository + * @param TransactionFactory $transactionFactory + */ + public function __construct( + InvoiceRepositoryInterface $invoiceRepository, + TransactionFactory $transactionFactory + ) { + $this->transactionFactory = $transactionFactory; + $this->invoiceRepository = $invoiceRepository; + } + + /** + * Add transaction comment to the order after capture invoice + * + * @param InvoiceService $subject + * @param bool $result + * @param int $invoiceId + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSetCapture(InvoiceService $subject, bool $result, $invoiceId): bool + { + if ($result) { + $invoice = $this->invoiceRepository->get($invoiceId); + $invoice->getOrder()->setIsInProcess(true); + $this->transactionFactory->create() + ->addObject($invoice) + ->addObject($invoice->getOrder()) + ->save(); + } + + return $result; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php b/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php new file mode 100644 index 0000000000000..8d1a2f5256370 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Plugin\Model\Service\Invoice; + +use Magento\Framework\DB\Transaction; +use Magento\Framework\DB\TransactionFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test to add transaction comment to the order after capture invoice + */ +class AddTransactionCommentAfterCaptureTest extends TestCase +{ + /** + * @var InvoiceRepositoryInterface|MockObject + */ + private $invoiceRepository; + + /** + * @var TransactionFactory|MockObject + */ + private $transactionFactory; + + /** + * @var AddTransactionCommentAfterCapture + */ + private $plugin; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->invoiceRepository = $this->createMock(InvoiceRepositoryInterface::class); + $this->transactionFactory = $this->createMock(TransactionFactory::class); + + $this->plugin = new AddTransactionCommentAfterCapture( + $this->invoiceRepository, + $this->transactionFactory + ); + } + + /** + * Test to add transaction comment after capture invoice + */ + public function testPlugin(): void + { + $result = true; + $invoiceId = 3; + + $orderMock = $this->createMock(Order::class); + $invoiceMock = $this->createMock(Invoice::class); + $invoiceMock->method('getOrder')->willReturn($orderMock); + $this->invoiceRepository->method('get')->with($invoiceId)->willReturn($invoiceMock); + + $transactionMock = $this->createMock(Transaction::class); + $transactionMock->expects($this->at(0))->method('addObject')->with($invoiceMock)->willReturnSelf(); + $transactionMock->expects($this->at(1))->method('addObject')->with($orderMock)->willReturnSelf(); + $transactionMock->expects($this->once())->method('save'); + $this->transactionFactory->method('create')->willReturn($transactionMock); + + /** @var InvoiceService $invoiceService */ + $invoiceService = $this->createMock(InvoiceService::class); + + $this->assertEquals( + $result, + $this->plugin->afterSetCapture($invoiceService, $result, $invoiceId) + ); + } +} diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 1a8478438b04a..71fc2cab22f13 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -22,4 +22,7 @@ <type name="Magento\Sales\Model\Order\ShipmentRepository"> <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> </type> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 1a8478438b04a..71fc2cab22f13 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -22,4 +22,7 @@ <type name="Magento\Sales\Model\Order\ShipmentRepository"> <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> </type> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> + </type> </config> From 4e40bc152a530d0bf549adb36817b1d5fced9e8b Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Mon, 12 Oct 2020 16:34:35 -0500 Subject: [PATCH 072/195] [AWS S3] MC-37463: Support by Magento Maintenance Mode (#6215) * MC-37479: Support by Magento Content Design * MC-37463: Support by Magento Maintenance Mode --- app/code/Magento/AwsS3/Driver/AwsS3.php | 45 +++++++- .../Magento/AwsS3/Driver/AwsS3Factory.php | 31 ++---- .../AwsS3/Test/Mftf/Data/ConfigData.xml | 17 +++ ...3AdminMarketingCreateSitemapEntityTest.xml | 61 +++++++++++ ...wsS3AdminMarketingSiteMapCreateNewTest.xml | 39 +++++++ .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 47 +++++++- app/code/Magento/AwsS3/etc/di.xml | 4 +- .../Command/AbstractMaintenanceCommand.php | 26 +++-- .../Command/MaintenanceAllowIpsCommand.php | 24 +++-- .../Command/MaintenanceDisableCommand.php | 10 +- .../Command/MaintenanceEnableCommand.php | 10 +- .../Command/MaintenanceStatusCommand.php | 17 +-- .../Magento/Backend/Console/CommandList.php | 64 +++++++++++ .../Backend/Model}/Validator/IpValidator.php | 2 +- .../MaintenanceAllowIpsCommandTest.php | 6 +- .../Command/MaintenanceDisableCommandTest.php | 6 +- .../Command/MaintenanceEnableCommandTest.php | 6 +- .../Command/MaintenanceStatusCommandTest.php | 4 +- .../Unit/Model}/Validator/IpValidatorTest.php | 20 ++-- app/code/Magento/Backend/cli_commands.php | 8 ++ app/code/Magento/Backend/composer.json | 3 +- app/code/Magento/Backend/etc/di.xml | 4 + .../Command/RemoteStorageDisableCommand.php | 74 +++++++++++++ .../Command/RemoteStorageEnableCommand.php | 100 ++++++++++++++---- .../Driver/DriverFactoryInterface.php | 4 +- .../Driver/DriverFactoryPool.php | 57 ++++++++++ .../RemoteStorage/Driver/DriverPool.php | 45 +++++--- .../Magento/RemoteStorage/Model/Config.php | 29 ++++- app/code/Magento/RemoteStorage/etc/di.xml | 6 ++ .../src/Magento/Setup/Console/CommandList.php | 4 - 30 files changed, 638 insertions(+), 135 deletions(-) create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/AbstractMaintenanceCommand.php (85%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceAllowIpsCommand.php (90%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceDisableCommand.php (84%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceEnableCommand.php (80%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceStatusCommand.php (85%) create mode 100644 app/code/Magento/Backend/Console/CommandList.php rename {setup/src/Magento/Setup => app/code/Magento/Backend/Model}/Validator/IpValidator.php (97%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php (96%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php (95%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php (93%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php (95%) rename {setup/src/Magento/Setup/Test/Unit => app/code/Magento/Backend/Test/Unit/Model}/Validator/IpValidatorTest.php (79%) create mode 100644 app/code/Magento/Backend/cli_commands.php create mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index a224d9d6ce0ef..8a862a6812107 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -195,9 +195,40 @@ public function readDirectory($path): array */ public function getRealPathSafety($path) { - return $this->normalizeAbsolutePath( - $this->normalizeRelativePath($path) + if (strpos($path, '/.') === false) { + return $path; + } + + $isAbsolute = strpos($path, $this->normalizeAbsolutePath()) === 0; + $path = $this->normalizeRelativePath($path); + + //Removing redundant directory separators. + $path = preg_replace( + '/\\/\\/+/', + '/', + $path ); + $pathParts = explode('/', $path); + if (end($pathParts) === '.') { + $pathParts[count($pathParts) - 1] = ''; + } + $realPath = []; + foreach ($pathParts as $pathPart) { + if ($pathPart === '.') { + continue; + } + if ($pathPart === '..') { + array_pop($realPath); + continue; + } + $realPath[] = $pathPart; + } + + if ($isAbsolute) { + return $this->normalizeAbsolutePath(implode('/', $realPath)); + } + + return implode('/', $realPath); } /** @@ -227,6 +258,14 @@ public function getAbsolutePath($basePath, $path, $scheme = null) private function normalizeAbsolutePath(string $path = '.'): string { $path = ltrim($path, '/'); + $path = str_replace( + $this->adapter->getClient()->getObjectUrl( + $this->adapter->getBucket(), + $this->adapter->applyPathPrefix('.') + ), + '', + $path + ); if (!$path) { $path = '.'; @@ -317,7 +356,7 @@ public function getRelativePath($basePath, $path = null): string public function getParentDirectory($path): string { //phpcs:ignore Magento2.Functions.DiscouragedFunction - return dirname($this->normalizeAbsolutePath($path)); + return rtrim(dirname($this->normalizeAbsolutePath($path)), '/') . '/'; } /** diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index 6ab8f93fa6e2b..d9efe6f7fd10e 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -9,7 +9,6 @@ use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; -use Magento\AwsS3\Model\Config; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\ObjectManagerInterface; use Magento\RemoteStorage\Driver\DriverFactoryInterface; @@ -24,43 +23,27 @@ class AwsS3Factory implements DriverFactoryInterface */ private $objectManager; - /** - * @var Config - */ - private $config; - /** * @param ObjectManagerInterface $objectManager - * @param Config $config */ - public function __construct(ObjectManagerInterface $objectManager, Config $config) + public function __construct(ObjectManagerInterface $objectManager) { $this->objectManager = $objectManager; - $this->config = $config; } /** * Creates an instance of AWS S3 driver. * + * @param array $config + * @param string $prefix * @return DriverInterface */ - public function create(): DriverInterface + public function create(array $config, string $prefix): DriverInterface { - $config = [ - 'region' => $this->config->getRegion(), + $config += [ 'version' => 'latest' ]; - $key = $this->config->getAccessKey(); - $secret = $this->config->getSecretKey(); - - if ($key && $secret) { - $config['credentials'] = [ - 'key' => $key, - 'secret' => $secret, - ]; - } - return $this->objectManager->create( AwsS3::class, [ @@ -68,8 +51,8 @@ public function create(): DriverInterface AwsS3Adapter::class, [ 'client' => $this->objectManager->create(S3Client::class, ['args' => $config]), - 'bucket' => $this->config->getBucket(), - 'prefix' => $this->config->getPrefix() + 'bucket' => $config['bucket'], + 'prefix' => $prefix ] ) ] diff --git a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..bc43aff37a491 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="RemoteStorageAwsS3ConfigData"> + <data key="driver">{{_ENV.REMOTE_STORAGE_AWSS3_DRIVER}}</data> + <data key="region">{{_ENV.REMOTE_STORAGE_AWSS3_REGION}}</data> + <data key="prefix">{{_ENV.REMOTE_STORAGE_AWSS3_PREFIX}}</data> + <data key="bucket">{{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}}</data> + <data key="access_key">{{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}}</data> + <data key="secret_key">{{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}}</data> + </entity> +</entities> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml new file mode 100644 index 0000000000000..4d411fcffc682 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml @@ -0,0 +1,61 @@ +<?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="AwsS3AdminMarketingCreateSitemapEntityTest"> + <annotations> + <features value="Sitemap"/> + <stories value="AWS S3 Admin Creates Sitemap Entity"/> + <title value="AWS S3 Sitemap Creation"/> + <description value="Sitemap Entity Creation"/> + <testCaseId value="MC-38319"/> + <severity value="MAJOR"/> + <group value="sitemap"/> + <group value="mtf_migrated"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteCreatedSitemap"> + <argument name="filename" value="sitemap.xml"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + + <!--TEST BODY --> + <!--Navigate to Marketing->Sitemap Page --> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSiteMapPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSEOAndSearchSiteMap.dataUiId}}"/> + </actionGroup> + <!-- Navigate to New Sitemap Creation Page --> + <actionGroup ref="AdminMarketingNavigateToNewSitemapPageActionGroup" stepKey="navigateToAddNewSitemap"/> + <!-- Create Sitemap Entity --> + <actionGroup ref="AdminMarketingCreateSitemapEntityActionGroup" stepKey="createSitemap"> + <argument name="filename" value="sitemap.xml"/> + <argument name="path" value="/"/> + </actionGroup> + <!-- Assert Success Message --> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeSuccessMessage"> + <argument name="message" value="You saved the sitemap."/> + <argument name="messageType" value="success"/> + </actionGroup> + <!-- Find Created Sitemap On Grid --> + <actionGroup ref="AdminMarketingSearchSitemapActionGroup" stepKey="findCreatedSitemapInGrid"> + <argument name="name" value="sitemap.xml"/> + </actionGroup> + <actionGroup ref="AssertAdminSitemapInGridActionGroup" stepKey="assertSitemapInGrid"> + <argument name="name" value="sitemap.xml"/> + </actionGroup> + <!--END TEST BODY --> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml new file mode 100644 index 0000000000000..43cf305e3fd17 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml @@ -0,0 +1,39 @@ +<?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="AwsS3AdminMarketingSiteMapCreateNewTest"> + <annotations> + <features value="Sitemap"/> + <stories value="AWS S3 Create Site Map"/> + <title value="AWS S3 Create New Site Map with valid data"/> + <description value="Create New Site Map with valid data"/> + <testCaseId value="MC-38320" /> + <severity value="CRITICAL"/> + <group value="sitemap"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteSiteMap"> + <argument name="filename" value="{{DefaultSiteMap.filename}}" /> + </actionGroup> + <actionGroup ref="AssertSiteMapDeleteSuccessActionGroup" stepKey="assertDeleteSuccessMessage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + <actionGroup ref="AdminMarketingSiteMapNavigateNewActionGroup" stepKey="navigateNewSiteMap"/> + <actionGroup ref="AdminMarketingSiteMapFillFormActionGroup" stepKey="fillSiteMapForm"> + <argument name="sitemap" value="DefaultSiteMap" /> + </actionGroup> + <actionGroup ref="AssertSiteMapCreateSuccessActionGroup" stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index 5ddc4811230ec..b3de684ed67dd 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -59,9 +59,7 @@ protected function setUp(): void return self::URL . $path; }); - $this->driver = new AwsS3( - $this->adapterMock - ); + $this->driver = new AwsS3($this->adapterMock); } /** @@ -116,6 +114,11 @@ public function getAbsolutePathDataProvider(): array self::URL . 'media/', '/catalog/test.png', self::URL . 'media/catalog/test.png' + ], + [ + '', + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' ] ]; } @@ -137,7 +140,7 @@ public function testGetRelativePath(string $basePath, string $path, string $expe */ public function getRelativePathDataProvider(): array { - return [ + return [ [ '', 'test/test.txt', @@ -324,4 +327,40 @@ public function isFileDataProvider(): array ] ]; } + + /** + * @param string $path + * @param string $expected + * + * @dataProvider getRealPathSafetyDataProvider + */ + public function testGetRealPathSafety(string $path, string $expected): void + { + self::assertSame($expected, $this->driver->getRealPathSafety($path)); + } + + /** + * @return array + */ + public function getRealPathSafetyDataProvider(): array + { + return [ + [ + self::URL, + self::URL + ], + [ + 'test.txt', + 'test.txt' + ], + [ + self::URL . 'test/test/../test.txt', + self::URL . 'test/test.txt' + ], + [ + 'test/test/../test.txt', + 'test/test.txt' + ] + ]; + } } diff --git a/app/code/Magento/AwsS3/etc/di.xml b/app/code/Magento/AwsS3/etc/di.xml index 2b66da74299ea..94df51fcd6856 100644 --- a/app/code/Magento/AwsS3/etc/di.xml +++ b/app/code/Magento/AwsS3/etc/di.xml @@ -6,9 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\RemoteStorage\Driver\DriverPool"> + <type name="Magento\RemoteStorage\Driver\DriverFactoryPool"> <arguments> - <argument name="remotePool" xsi:type="array"> + <argument name="pool" xsi:type="array"> <item name="aws-s3" xsi:type="object">Magento\AwsS3\Driver\AwsS3Factory</item> </argument> </arguments> diff --git a/setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php b/app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php similarity index 85% rename from setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php rename to app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php index 85ae008adf366..eb452f62e91ce 100644 --- a/setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php +++ b/app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php @@ -3,14 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Validator\IpValidator; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; +use Magento\Backend\Model\Validator\IpValidator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * General maintenance command. + */ abstract class AbstractMaintenanceCommand extends AbstractSetupCommand { /** @@ -38,6 +43,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal { $this->maintenanceMode = $maintenanceMode; $this->ipValidator = $ipValidator; + parent::__construct(); } @@ -57,6 +63,7 @@ protected function configure() ), ]; $this->setDefinition($options); + parent::configure(); } @@ -75,16 +82,18 @@ abstract protected function isEnable(); abstract protected function getDisplayString(); /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $addresses = $input->getOption(self::INPUT_KEY_IP); $messages = $this->validate($addresses); + if (!empty($messages)) { $output->writeln('<error>' . implode('</error>' . PHP_EOL . '<error>', $messages)); - // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + + // We must have an exit code higher than zero to indicate something was wrong + return Cli::RETURN_FAILURE; } $this->maintenanceMode->set($this->isEnable()); @@ -92,14 +101,15 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!empty($addresses)) { $addresses = implode(',', $addresses); - $addresses = ('none' == $addresses) ? '' : $addresses; + $addresses = ('none' === $addresses) ? '' : $addresses; $this->maintenanceMode->setAddresses($addresses); $output->writeln( '<info>Set exempt IP-addresses: ' . (implode(', ', $this->maintenanceMode->getAddressInfo()) ?: 'none') . '</info>' ); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php similarity index 90% rename from setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php index 09f33cf85062c..230c6a6814ebc 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Framework\Module\ModuleList; -use Magento\Setup\Validator\IpValidator; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; +use Magento\Backend\Model\Validator\IpValidator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -37,8 +37,6 @@ class MaintenanceAllowIpsCommand extends AbstractSetupCommand private $ipValidator; /** - * Constructor - * * @param MaintenanceMode $maintenanceMode * @param IpValidator $ipValidator */ @@ -46,6 +44,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal { $this->maintenanceMode = $maintenanceMode; $this->ipValidator = $ipValidator; + parent::__construct(); } @@ -54,7 +53,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal * * @return void */ - protected function configure() + protected function configure(): void { $arguments = [ new InputArgument( @@ -80,19 +79,21 @@ protected function configure() $this->setName('maintenance:allow-ips') ->setDescription('Sets maintenance mode exempt IPs') ->setDefinition(array_merge($arguments, $options)); + parent::configure(); } /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { if (!$input->getOption(self::INPUT_KEY_NONE)) { $addresses = $input->getArgument(self::INPUT_KEY_IP); $messages = $this->validate($addresses); if (!empty($messages)) { $output->writeln('<error>' . implode('</error>' . PHP_EOL . '<error>', $messages)); + // we must have an exit code higher than zero to indicate something was wrong return \Magento\Framework\Console\Cli::RETURN_FAILURE; } @@ -111,7 +112,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->maintenanceMode->setAddresses(''); $output->writeln('<info>Set exempt IP-addresses: none</info>'); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** @@ -120,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output) * @param string[] $addresses * @return string[] */ - protected function validate(array $addresses) + protected function validate(array $addresses): array { return $this->ipValidator->validateIps($addresses, false); } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php similarity index 84% rename from setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php index abebbdb76346b..5108866fbe65c 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; /** * Command for disabling maintenance mode @@ -19,6 +18,7 @@ class MaintenanceDisableCommand extends AbstractMaintenanceCommand protected function configure() { $this->setName('maintenance:disable')->setDescription('Disables maintenance mode'); + parent::configure(); } @@ -27,7 +27,7 @@ protected function configure() * * @return bool */ - protected function isEnable() + protected function isEnable(): bool { return false; } @@ -37,7 +37,7 @@ protected function isEnable() * * @return string */ - protected function getDisplayString() + protected function getDisplayString(): string { return '<info>Disabled maintenance mode</info>'; } @@ -47,7 +47,7 @@ protected function getDisplayString() * * @return bool */ - public function isSetAddressInfo() + public function isSetAddressInfo(): bool { return count($this->maintenanceMode->getAddressInfo()) > 0; } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php similarity index 80% rename from setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php index 94ab312b60811..7e5e034483d20 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; /** * Command for enabling maintenance mode @@ -16,9 +15,10 @@ class MaintenanceEnableCommand extends AbstractMaintenanceCommand * * @return void */ - protected function configure() + protected function configure(): void { $this->setName('maintenance:enable')->setDescription('Enables maintenance mode'); + parent::configure(); } @@ -27,7 +27,7 @@ protected function configure() * * @return bool */ - protected function isEnable() + protected function isEnable(): bool { return true; } @@ -37,7 +37,7 @@ protected function isEnable() * * @return string */ - protected function getDisplayString() + protected function getDisplayString(): string { return '<info>Enabled maintenance mode</info>'; } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php similarity index 85% rename from setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php index f2d3d2bf30caa..e7feae32cf8b0 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php @@ -3,11 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Framework\Module\ModuleList; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -29,6 +29,7 @@ class MaintenanceStatusCommand extends AbstractSetupCommand public function __construct(MaintenanceMode $maintenanceMode) { $this->maintenanceMode = $maintenanceMode; + parent::__construct(); } @@ -37,17 +38,18 @@ public function __construct(MaintenanceMode $maintenanceMode) * * @return void */ - protected function configure() + protected function configure(): void { $this->setName('maintenance:status') ->setDescription('Displays maintenance mode status'); + parent::configure(); } /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln( '<info>Status: maintenance mode is ' . @@ -56,6 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $addressInfo = $this->maintenanceMode->getAddressInfo(); $addresses = implode(' ', $addressInfo); $output->writeln('<info>List of exempt IP-addresses: ' . ($addresses ? $addresses : 'none') . '</info>'); - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/Backend/Console/CommandList.php b/app/code/Magento/Backend/Console/CommandList.php new file mode 100644 index 0000000000000..563ef964812ab --- /dev/null +++ b/app/code/Magento/Backend/Console/CommandList.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Console; + +use Magento\Backend\Console\Command\MaintenanceAllowIpsCommand; +use Magento\Backend\Console\Command\MaintenanceDisableCommand; +use Magento\Backend\Console\Command\MaintenanceEnableCommand; +use Magento\Backend\Console\Command\MaintenanceStatusCommand; +use Magento\Framework\Console\CommandListInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Provides list of commands to be available for uninstalled application + */ +class CommandList implements CommandListInterface +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Gets list of command classes + * + * @return string[] + */ + private function getCommandsClasses(): array + { + return [ + MaintenanceAllowIpsCommand::class, + MaintenanceDisableCommand::class, + MaintenanceEnableCommand::class, + MaintenanceStatusCommand::class + ]; + } + + /** + * @inheritdoc + */ + public function getCommands(): array + { + $commands = []; + foreach ($this->getCommandsClasses() as $class) { + if (class_exists($class)) { + $commands[] = $this->objectManager->get($class); + } else { + throw new \RuntimeException('Class ' . $class . ' does not exist'); + } + } + + return $commands; + } +} diff --git a/setup/src/Magento/Setup/Validator/IpValidator.php b/app/code/Magento/Backend/Model/Validator/IpValidator.php similarity index 97% rename from setup/src/Magento/Setup/Validator/IpValidator.php rename to app/code/Magento/Backend/Model/Validator/IpValidator.php index 5d1e83021e34b..f208d02ee140a 100644 --- a/setup/src/Magento/Setup/Validator/IpValidator.php +++ b/app/code/Magento/Backend/Model/Validator/IpValidator.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Setup\Validator; +namespace Magento\Backend\Model\Validator; /** * Class to validate list of IPs for maintenance commands diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php similarity index 96% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php index 2a18a892ed06d..281065c51337d 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceAllowIpsCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceAllowIpsCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php similarity index 95% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php index 73afa22f3ebcd..6663a7f9f6504 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceDisableCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceDisableCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php similarity index 93% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php index 0b1afb7310c08..c4a2e35d37d49 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceEnableCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceEnableCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php similarity index 95% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php index 731eff370b00f..8e3970aa5529e 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php @@ -5,10 +5,10 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceStatusCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceStatusCommand; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php similarity index 79% rename from setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php rename to app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php index b6f9f01c80ee5..ccffc58d79780 100644 --- a/setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php @@ -5,11 +5,14 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Validator; +namespace Magento\Backend\Test\Unit\Model\Validator; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\TestCase; +/** + * @see IpValidator + */ class IpValidatorTest extends TestCase { /** @@ -17,6 +20,9 @@ class IpValidatorTest extends TestCase */ private $ipValidator; + /** + * @inheritDoc + */ protected function setUp(): void { $this->ipValidator = new IpValidator(); @@ -27,15 +33,15 @@ protected function setUp(): void * @param string[] $ips * @param string[] $expectedMessages */ - public function testValidateIpsNoneAllowed($ips, $expectedMessages) + public function testValidateIpsNoneAllowed(array $ips, array $expectedMessages): void { - $this->assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, true)); + self::assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, true)); } /** * @return array */ - public function validateIpsNoneAllowedDataProvider() + public function validateIpsNoneAllowedDataProvider(): array { return [ [['127.0.0.1', '127.0.0.2'], []], @@ -54,9 +60,9 @@ public function validateIpsNoneAllowedDataProvider() * @param string[] $ips * @param string[] $expectedMessages */ - public function testValidateIpsNoneNotAllowed($ips, $expectedMessages) + public function testValidateIpsNoneNotAllowed($ips, $expectedMessages): void { - $this->assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, false)); + self::assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, false)); } /** diff --git a/app/code/Magento/Backend/cli_commands.php b/app/code/Magento/Backend/cli_commands.php new file mode 100644 index 0000000000000..3c4140b40a993 --- /dev/null +++ b/app/code/Magento/Backend/cli_commands.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +if (PHP_SAPI === 'cli') { + \Magento\Framework\Console\CommandLocator::register(\Magento\Backend\Console\CommandList::class); +} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index ee5491057d861..017f247adbf43 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -34,7 +34,8 @@ ], "autoload": { "files": [ - "registration.php" + "registration.php", + "cli_commands.php" ], "psr-4": { "Magento\\Backend\\": "" diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml index 65f73f028eb20..1297bd9603a1f 100644 --- a/app/code/Magento/Backend/etc/di.xml +++ b/app/code/Magento/Backend/etc/di.xml @@ -150,6 +150,10 @@ <item name="cacheFlushCommand" xsi:type="object">Magento\Backend\Console\Command\CacheFlushCommand</item> <item name="cacheCleanCommand" xsi:type="object">Magento\Backend\Console\Command\CacheCleanCommand</item> <item name="cacheStatusCommand" xsi:type="object">Magento\Backend\Console\Command\CacheStatusCommand</item> + <item name="maintenanceAllowIps" xsi:type="object">Magento\Backend\Console\Command\MaintenanceAllowIpsCommand</item> + <item name="maintenanceDisable" xsi:type="object">Magento\Backend\Console\Command\MaintenanceDisableCommand</item> + <item name="maintenanceEnableCommand" xsi:type="object">Magento\Backend\Console\Command\MaintenanceDisableCommand</item> + <item name="maintenanceStatusCommand" xsi:type="object">Magento\Backend\Console\Command\MaintenanceStatusCommand</item> </argument> </arguments> </type> diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php new file mode 100644 index 0000000000000..e87ca584299e7 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverPool; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Remote storage configuration disablement. + */ +class RemoteStorageDisableCommand extends Command +{ + private const NAME = 'remote-storage:disable'; + + /** + * @var Writer + */ + private $writer; + + /** + * @param Writer $writer + */ + public function __construct(Writer $writer) + { + $this->writer = $writer; + + parent::__construct(); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Disable remote storage'); + } + + /** + * Executes command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->writer->saveConfig([ + ConfigFilePool::APP_ENV => [ + 'remote_storage' => [ + 'driver' => DriverPool::FILE, + ] + ] + ], true); + + $output->writeln('<info>Config was saved.</info>'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php index cad5ecf314da2..e308f609a3d69 100644 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php @@ -9,7 +9,10 @@ use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Console\Cli; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverFactoryPool; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -23,11 +26,11 @@ class RemoteStorageEnableCommand extends Command { private const NAME = 'remote-storage:enable'; private const ARG_DRIVER = 'driver'; - private const OPTION_BUCKET = 'bucket'; - private const OPTION_REGION = 'region'; - private const OPTION_ACCESS_KEY = 'access-key'; - private const OPTION_SECRET_KEY = 'secret-key'; - private const OPTION_PREFIX = 'prefix'; + private const ARGUMENT_BUCKET = 'bucket'; + private const ARGUMENT_REGION = 'region'; + private const ARGUMENT_ACCESS_KEY = 'access-key'; + private const ARGUMENT_SECRET_KEY = 'secret-key'; + private const ARGUMENT_PREFIX = 'prefix'; private const OPTION_IS_PUBLIC = 'is-public'; /** @@ -35,12 +38,19 @@ class RemoteStorageEnableCommand extends Command */ private $writer; + /** + * @var DriverFactoryPool + */ + private $driverFactoryPool; + /** * @param Writer $writer + * @param DriverFactoryPool $driverFactoryPool */ - public function __construct(Writer $writer) + public function __construct(Writer $writer, DriverFactoryPool $driverFactoryPool) { $this->writer = $writer; + $this->driverFactoryPool = $driverFactoryPool; parent::__construct(); } @@ -52,13 +62,13 @@ protected function configure(): void { $this->setName(self::NAME) ->setDescription('Enable remote storage integration') - ->addArgument(self::ARG_DRIVER, InputArgument::REQUIRED, 'Remote driver') - ->addOption(self::OPTION_BUCKET, null, InputOption::VALUE_REQUIRED, 'Bucket') - ->addOption(self::OPTION_REGION, null, InputOption::VALUE_REQUIRED, 'Region') - ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_REQUIRED, 'Access key') - ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_REQUIRED, 'Secret key') - ->addOption(self::OPTION_PREFIX, null, InputOption::VALUE_REQUIRED, 'Prefix', '') - ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_NONE, 'Is public'); + ->addArgument(self::ARG_DRIVER, InputArgument::OPTIONAL, 'Remote driver', DriverPool::FILE) + ->addArgument(self::ARGUMENT_BUCKET, InputArgument::OPTIONAL, 'Bucket') + ->addArgument(self::ARGUMENT_REGION, InputArgument::OPTIONAL, 'Region') + ->addArgument(self::ARGUMENT_PREFIX, InputArgument::OPTIONAL, 'Prefix', '') + ->addArgument(self::ARGUMENT_ACCESS_KEY, InputArgument::OPTIONAL, 'Access key') + ->addArgument(self::ARGUMENT_SECRET_KEY, InputArgument::OPTIONAL, 'Secret key') + ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_REQUIRED, 'Is public', false); } /** @@ -66,25 +76,69 @@ protected function configure(): void * * @param InputInterface $input * @param OutputInterface $output - * @return void + * @return int * @throws FileSystemException */ - public function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { + $driver = $input->getArgument(self::ARG_DRIVER); + + if ($driver === DriverPool::FILE) { + $output->writeln(sprintf( + 'Driver "%s" was specified. Skipping', + $driver + )); + + return Cli::RETURN_SUCCESS; + } + + if (!$this->driverFactoryPool->has($driver)) { + $output->writeln('Driver %s was not found', $driver); + + return Cli::RETURN_FAILURE; + } + + $prefix = (string)$input->getArgument(self::ARGUMENT_PREFIX); + $config = [ + 'bucket' => (string)$input->getArgument(self::ARGUMENT_BUCKET), + 'region' => (string)$input->getArgument(self::ARGUMENT_REGION), + ]; + $isPublic = (bool)$input->getOption(self::OPTION_IS_PUBLIC); + + if (($key = (string)$input->getArgument(self::ARGUMENT_ACCESS_KEY)) + && ($secret = (string)$input->getArgument(self::ARGUMENT_SECRET_KEY)) + ) { + $config['credentials']['key'] = $key; + $config['credentials']['secret'] = $secret; + } + + try { + $this->driverFactoryPool->get($driver)->create($config, $prefix); + } catch (\Exception $exception) { + $output->writeln(sprintf( + '<error>Config cannot be set: %s</error>', + $exception->getMessage() + )); + + return Cli::RETURN_FAILURE; + } + $this->writer->saveConfig([ ConfigFilePool::APP_ENV => [ 'remote_storage' => [ - 'driver' => (string)$input->getArgument(self::ARG_DRIVER), - 'bucket' => (string)$input->getOption(self::OPTION_BUCKET), - 'region' => (string)$input->getOption(self::OPTION_REGION), - 'access_key' => (string)$input->getOption(self::OPTION_ACCESS_KEY), - 'secret_key' => (string)$input->getOption(self::OPTION_SECRET_KEY), - 'prefix' => (string)$input->getOption(self::OPTION_PREFIX), - 'is_public' => (bool)$input->getOption(self::OPTION_IS_PUBLIC) + 'driver' => $driver, + 'prefix' => $prefix, + 'is_public' => $isPublic, + 'config' => $config ] ] ], true); - $output->writeln('<info>Config was saved.</info>'); + $output->writeln(sprintf( + '<info>Config for driver "%s" was saved.</info>', + $driver + )); + + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php index a95284fb27391..ab7a1bcaa6cc5 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -17,7 +17,9 @@ interface DriverFactoryInterface /** * Creates pre-configured driver. * + * @param array $config + * @param string $prefix * @return DriverInterface */ - public function create(): DriverInterface; + public function create(array $config, string $prefix): DriverInterface; } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php new file mode 100644 index 0000000000000..aa4e057af5383 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\RuntimeException; + +/** + * Pool of driver factories. + */ +class DriverFactoryPool +{ + /** + * @var DriverFactoryInterface[] + */ + private $pool; + + /** + * @param DriverFactoryInterface[] $pool + */ + public function __construct(array $pool) + { + $this->pool = $pool; + } + + /** + * Check if factory exists. + * + * @param string $name + * @return bool + */ + public function has(string $name): bool + { + return isset($this->pool[$name]); + } + + /** + * Retrieve factory. + * + * @param string $name + * @return DriverFactoryInterface + * + * @throws RuntimeException + */ + public function get(string $name): DriverFactoryInterface + { + if (!$this->has($name)) { + throw new RuntimeException(__('Factory %1 does not exist', $name)); + } + + return $this->pool[$name]; + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index 4c2c834f0d776..a1e758f170e7e 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -21,38 +21,47 @@ class DriverPool implements DriverPoolInterface { public const PATH_DRIVER = 'remote_storage/driver'; public const PATH_IS_PUBLIC = 'remote_storage/is_public'; + public const PATH_PREFIX = 'remote_storage/prefix'; + public const PATH_CONFIG = 'remote_storage/config'; + + /** + * Driver name. + */ public const REMOTE = 'remote'; /** - * @var DriverPool + * @var Config */ - private $driverPool; + private $config; /** - * @var DriverInterface[] + * @var DriverFactoryPool */ - private $pool = []; + private $driverFactoryPool; /** - * @var DriverFactoryInterface[] + * @var DriverPool */ - private $remotePool; + private $driverPool; /** - * @var Config + * @var array */ - private $config; + private $pool = []; /** - * @param BaseDriverPool $driverPool * @param Config $config - * @param array $remotePool + * @param DriverFactoryPool $driverFactoryPool + * @param BaseDriverPool $driverPool */ - public function __construct(BaseDriverPool $driverPool, Config $config, array $remotePool = []) - { - $this->driverPool = $driverPool; + public function __construct( + Config $config, + DriverFactoryPool $driverFactoryPool, + BaseDriverPool $driverPool + ) { $this->config = $config; - $this->remotePool = $remotePool; + $this->driverFactoryPool = $driverFactoryPool; + $this->driverPool = $driverPool; } /** @@ -60,7 +69,6 @@ public function __construct(BaseDriverPool $driverPool, Config $config, array $r * * @param string $code * @return DriverInterface - * * @throws RuntimeException * @throws FileSystemException */ @@ -73,8 +81,11 @@ public function getDriver($code = self::REMOTE): DriverInterface $driver = $this->config->getDriver(); - if ($driver && isset($this->remotePool[$driver])) { - return $this->pool[$code] = $this->remotePool[$driver]->create(); + if ($driver && $this->driverFactoryPool->has($driver)) { + return $this->pool[$code] = $this->driverFactoryPool->get($driver)->create( + $this->config->getConfig(), + $this->config->getPrefix() + ); } throw new RuntimeException(__('Remote driver is not available.')); diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php index 164d94cefddee..36238b20b38bb 100644 --- a/app/code/Magento/RemoteStorage/Model/Config.php +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -11,6 +11,7 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\RuntimeException; use Magento\RemoteStorage\Driver\DriverPool; +use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; /** * Configuration for remote storage. @@ -51,7 +52,9 @@ public function getDriver(): ?string */ public function isEnabled(): bool { - return $this->config->get(DriverPool::PATH_DRIVER) !== null; + $driver = $this->config->get(DriverPool::PATH_DRIVER); + + return $driver && $driver !== BaseDriverPool::FILE; } /** @@ -65,4 +68,28 @@ public function isPublic(): bool { return (bool)$this->config->get(DriverPool::PATH_IS_PUBLIC, false); } + + /** + * Retrieves config. + * + * @return array + * @throws FileSystemException + * @throws RuntimeException + */ + public function getConfig(): array + { + return (array)$this->config->get(DriverPool::PATH_CONFIG, []); + } + + /** + * Retrieves prefix. + * + * @return string + * @throws FileSystemException + * @throws RuntimeException + */ + public function getPrefix(): string + { + return (string)$this->config->get(DriverPool::PATH_PREFIX, ''); + } } diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index a43a6160c554f..d9124326e65c2 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -65,7 +65,13 @@ <arguments> <argument name="commands" xsi:type="array"> <item name="remoteStorageEnable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageEnableCommand</item> + <item name="remoteStorageDisable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageDisableCommand</item> </argument> </arguments> </type> + <type name="Magento\Framework\App\MaintenanceMode"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> </config> diff --git a/setup/src/Magento/Setup/Console/CommandList.php b/setup/src/Magento/Setup/Console/CommandList.php index ab31a3add07ed..ae65e82bba12b 100644 --- a/setup/src/Magento/Setup/Console/CommandList.php +++ b/setup/src/Magento/Setup/Console/CommandList.php @@ -66,10 +66,6 @@ protected function getCommandsClasses() \Magento\Setup\Console\Command\ModuleStatusCommand::class, \Magento\Setup\Console\Command\ModuleUninstallCommand::class, \Magento\Setup\Console\Command\ModuleConfigStatusCommand::class, - \Magento\Setup\Console\Command\MaintenanceAllowIpsCommand::class, - \Magento\Setup\Console\Command\MaintenanceDisableCommand::class, - \Magento\Setup\Console\Command\MaintenanceEnableCommand::class, - \Magento\Setup\Console\Command\MaintenanceStatusCommand::class, \Magento\Setup\Console\Command\RollbackCommand::class, \Magento\Setup\Console\Command\UpgradeCommand::class, \Magento\Setup\Console\Command\UninstallCommand::class, From 8090eb0eafdabd3462a28f7fe47554fc9ff57a1c Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 13 Oct 2020 10:13:15 +0300 Subject: [PATCH 073/195] MC-38074: Report - Products in Carts not following user roles scope --- .../Reports/Model/Product/DataRetriever.php | 106 ++++++++++++++++++ .../ResourceModel/Quote/Item/Collection.php | 18 ++- .../Report/Quote/CollectionTest.php | 44 ++++---- .../Model/Product/DataRetrieverTest.php | 54 +++++++++ 4 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 app/code/Magento/Reports/Model/Product/DataRetriever.php create mode 100644 dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php diff --git a/app/code/Magento/Reports/Model/Product/DataRetriever.php b/app/code/Magento/Reports/Model/Product/DataRetriever.php new file mode 100644 index 0000000000000..c6260a4e7bacc --- /dev/null +++ b/app/code/Magento/Reports/Model/Product/DataRetriever.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Retrieve products data for reports by entity id's + */ +class DataRetriever +{ + /** + * @var ProductCollectionFactory + */ + private $productCollectionFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * DataRetriever constructor. + * + * @param ProductCollectionFactory $productCollectionFactory + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ProductCollectionFactory $productCollectionFactory, + StoreManagerInterface $storeManager + ) { + $this->productCollectionFactory = $productCollectionFactory; + $this->storeManager = $storeManager; + } + + /** + * Retrieve products data by entity id's + * + * @param array $entityIds + * @return array + */ + public function execute(array $entityIds = []): array + { + $productCollection = $this->getProductCollection($entityIds); + + return $this->prepareDataByCollection($productCollection); + } + + /** + * Get product collection filtered by entity id's + * + * @param array $entityIds + * @return ProductCollection + */ + private function getProductCollection(array $entityIds = []): ProductCollection + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect('name'); + $productCollection->addIdFilter($entityIds); + $productCollection->addPriceData(null, $this->getWebsiteIdForFilter()); + + return $productCollection; + } + + /** + * Retrieve website id for filter collection + * + * @return int + */ + private function getWebsiteIdForFilter(): int + { + $defaultStoreView = $this->storeManager->getDefaultStoreView(); + if ($defaultStoreView) { + $websiteId = (int)$defaultStoreView->getWebsiteId(); + } else { + $websites = $this->storeManager->getWebsites(); + $website = reset($websites); + $websiteId = (int)$website->getId(); + } + + return $websiteId; + } + + /** + * Prepare data by collection + * + * @param ProductCollection $productCollection + * @return array + */ + private function prepareDataByCollection(ProductCollection $productCollection): array + { + $productsData = []; + foreach ($productCollection as $product) { + $productsData[$product->getId()] = $product->getData(); + } + + return $productsData; + } +} diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php index 16df2d30db40d..e7dc28eb74a49 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php @@ -7,7 +7,8 @@ namespace Magento\Reports\Model\ResourceModel\Quote\Item; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; +use Magento\Reports\Model\Product\DataRetriever as ProductDataRetriever; /** * Collection of Magento\Quote\Model\Quote\Item @@ -49,6 +50,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab */ protected $orderResource; + /** + * @var ProductDataRetriever + */ + private $productDataRetriever; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -59,6 +65,9 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @param \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource + * @param ProductDataRetriever|null $productDataRetriever + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -69,7 +78,8 @@ public function __construct( \Magento\Customer\Model\ResourceModel\Customer $customerResource, \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null + \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null, + ?ProductDataRetriever $productDataRetriever = null ) { parent::__construct( $entityFactory, @@ -82,6 +92,8 @@ public function __construct( $this->productResource = $productResource; $this->customerResource = $customerResource; $this->orderResource = $orderResource; + $this->productDataRetriever = $productDataRetriever + ?? ObjectManager::getInstance()->get(ProductDataRetriever::class); } /** @@ -225,7 +237,7 @@ protected function _afterLoad() foreach ($items as $item) { $productIds[] = $item->getProductId(); } - $productData = $this->getProductData($productIds); + $productData = $this->productDataRetriever->execute($productIds); $orderData = $this->getOrdersData($productIds); foreach ($items as $item) { $item->setId($item->getProductId()); diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php index 6e7d5bdce16f5..90d224ee417db 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php @@ -7,15 +7,17 @@ namespace Magento\Reports\Test\Unit\Model\ResourceModel\Report\Quote; -use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\ResourceModel\Quote; -use Magento\Reports\Model\ResourceModel\Quote\Collection; +use Magento\Reports\Model\Product\DataRetriever as ProductDataRetriever; +use Magento\Reports\Model\ResourceModel\Quote\Collection as QuoteCollection; +use Magento\Reports\Model\ResourceModel\Quote\Item\Collection as QuoteItemCollection; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -34,16 +36,22 @@ class CollectionTest extends TestCase */ protected $selectMock; + /** + * @var ProductDataRetriever|MockObject + */ + private $productDataRetriever; + protected function setUp(): void { $this->objectManager = new ObjectManager($this); $this->selectMock = $this->createMock(Select::class); + $this->productDataRetriever = $this->createMock(ProductDataRetriever::class); } public function testGetSelectCountSql() { /** @var MockObject $collection */ - $collection = $this->getMockBuilder(Collection::class) + $collection = $this->getMockBuilder(QuoteCollection::class) ->setMethods(['getSelect']) ->disableOriginalConstructor() ->getMock(); @@ -61,8 +69,8 @@ public function testPrepareActiveCartItems() { /** @var MockObject $collection */ $constructArgs = $this->objectManager - ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) + ->getConstructArguments(QuoteItemCollection::class); + $collection = $this->getMockBuilder(QuoteItemCollection::class) ->setMethods(['getSelect', 'getTable', 'getFlag', 'setFlag']) ->disableOriginalConstructor() ->setConstructorArgs($constructArgs) @@ -88,18 +96,18 @@ public function testLoadWithFilter() { /** @var MockObject $collection */ $constructArgs = $this->objectManager - ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); + ->getConstructArguments(QuoteItemCollection::class); $constructArgs['eventManager'] = $this->getMockForAbstractClass(ManagerInterface::class); - $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $resourceMock = $this->createMock(Quote::class); $resourceMock->expects($this->any())->method('getConnection') ->willReturn($this->createMock(Mysql::class)); $constructArgs['resource'] = $resourceMock; - $productResourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + $productResourceMock = $this->createMock(ProductCollection::class); $constructArgs['productResource'] = $productResourceMock; - $orderResourceMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $orderResourceMock = $this->createMock(OrderCollection::class); $constructArgs['orderResource'] = $orderResourceMock; - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) + $constructArgs['productDataRetriever'] = $this->productDataRetriever; + $collection = $this->getMockBuilder(QuoteItemCollection::class) ->setMethods( [ '_beforeLoad', @@ -129,24 +137,12 @@ public function testLoadWithFilter() //productLoad() $productAttributeMock = $this->createMock(AbstractAttribute::class); $priceAttributeMock = $this->createMock(AbstractAttribute::class); - $productResourceMock->expects($this->once())->method('getConnection')->willReturn($connectionMock); $productResourceMock->expects($this->any())->method('getAttribute') ->willReturnMap([['name', $productAttributeMock], ['price', $priceAttributeMock]]); - $productResourceMock->expects($this->once())->method('getSelect')->willReturn($this->selectMock); - $eavEntity = $this->createMock(AbstractEntity::class); - $eavEntity->expects($this->once())->method('getLinkField')->willReturn('entity_id'); - $productResourceMock->expects($this->once())->method('getEntity')->willReturn($eavEntity); - $this->selectMock->expects($this->once())->method('reset')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('useStraightJoin')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('joinInner')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('joinLeft')->willReturnSelf(); $collection->expects($this->once())->method('getOrdersData')->willReturn([]); - $productAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf(); - $priceAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf(); - $connectionMock->expects($this->once())->method('fetchAssoc')->willReturn([1, 2, 3]); //_afterLoad() $collection->expects($this->once())->method('getItems')->willReturn([]); + $this->productDataRetriever->expects($this->once())->method('execute')->willReturn([]); $collection->loadWithFilter(); } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php b/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php new file mode 100644 index 0000000000000..086078685ae94 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model\Product; + +use Magento\Catalog\Model\Indexer\Product\Price\Processor; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + */ +class DataRetrieverTest extends TestCase +{ + /** + * @var DataRetriever + */ + private $dataRetriever; + + /** + * @var Processor + */ + private $priceIndexerProcessor; + + protected function setUp(): void + { + $this->dataRetriever = Bootstrap::getObjectManager()->create(DataRetriever::class); + $this->priceIndexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + } + + /** + * Test retrieve products data for reports by entity id's + * Do not use magentoDbIsolation because index statement changing "tears" transaction (triggers creating) + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default/reports/options/enabled 1 + * @magentoDbIsolation disabled + * + * @return void + */ + public function testExecute(): void + { + $productId = 1; + $this->priceIndexerProcessor->reindexAll(); + $actualResult = $this->dataRetriever->execute([$productId]); + $this->assertNotEmpty($actualResult); + $this->assertCount(1, $actualResult); + $this->assertEquals(10, $actualResult[$productId]['price']); + } +} From 8f92a813b9132e75af4f54747959b44b6cfc9872 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 13 Oct 2020 10:55:53 +0300 Subject: [PATCH 074/195] MC-38269: [CLARIFICATION] [Magento Cloud] - Persistent Shopping cart Header Weelcome & Not you? --- .../Persistent/view/frontend/templates/additional.phtml | 7 ++----- .../view/frontend/web/js/view/additional-welcome.js | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml index 40c8674bc025a..0c19a1c5c8b05 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -4,13 +4,10 @@ * See COPYING.txt for license details. */ ?> -<?php if ($block->getCustomerId()) :?> - <span> - <a <?= /* @noEscape */ $block->getLinkAttributes()?>><?= $block->escapeHtml(__('Not you?'));?></a> - </span> -<?php endif;?> <script type="application/javascript"> window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; + window.notYou = '<span> <a <?= /* @noEscape */ $block->getLinkAttributes()?>>' + + '<?= $block->escapeHtml(__("Not you?"));?></a></span>'; </script> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index 7ace6e60d1c39..fb57d311e35c3 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,6 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); + $(this).after(window.notYou); }); } } From 101b711b00e64e35bdf4f41b2ac4a9ca043701ed Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 13 Oct 2020 15:07:18 +0300 Subject: [PATCH 075/195] MC-38315: Reorder is not working with custom options date after enabled JavaScript Calendar --- .../Model/Product/Option/Type/Date.php | 40 +++++++ .../Magento/Sales/Model/AdminOrder/Create.php | 21 +--- .../Adminhtml/Order/Create/ReorderTest.php | 108 ++++++++++++++++++ .../order_with_date_time_option_product.php | 97 ++++++++++++++++ ...with_date_time_option_product_rollback.php | 12 ++ 5 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 8001d692c011b..725635bf4fc45 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -72,6 +72,9 @@ public function validateUserValue($values) $dateValid = true; if ($this->_dateExists()) { if ($this->useCalendar()) { + if (is_array($value) && $this->checkDateWithoutJSCalendar($value)) { + $value['date'] = sprintf("%s/%s/%s", $value['day'], $value['month'], $value['year']); + } /* Fixed validation if the date was not saved correctly after re-saved the order for example: "09\/24\/2020,2020-09-24 00:00:00" */ if (is_string($value) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4},+(\w|\W)*$/', $value)) { @@ -81,6 +84,9 @@ public function validateUserValue($values) } $dateValid = isset($value['date']) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4}$/', $value['date']); } else { + if (is_array($value)) { + $value = $this->prepareDateByDateInternal($value); + } $dateValid = isset( $value['day'] ) && isset( @@ -411,4 +417,38 @@ protected function _timeExists() ] ); } + + /** + * Check is date without JS Calendar + * + * @param array $value + * + * @return bool + */ + private function checkDateWithoutJSCalendar(array $value): bool + { + return empty($value['date']) + && !empty($value['day']) + && !empty($value['month']) + && !empty($value['year']); + } + + /** + * Prepare date by date internal + * + * @param array $value + * @return array + */ + private function prepareDateByDateInternal(array $value): array + { + if (!empty($value['date']) && !empty($value['date_internal'])) { + $formatDate = explode(' ', $value['date_internal']); + $date = explode('-', $formatDate[0]); + $value['year'] = $date[0]; + $value['month'] = $date[1]; + $value['day'] = $date[2]; + } + + return $value; + } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 393d61b69bf22..bbbe2bee1b205 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -667,12 +667,14 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q $productOptions = $orderItem->getProductOptions(); if ($productOptions !== null && !empty($productOptions['options'])) { $formattedOptions = []; - $useFrontendCalendar = $this->useFrontendCalendar(); foreach ($productOptions['options'] as $option) { - if (in_array($option['option_type'], ['date', 'date_time']) && $useFrontendCalendar) { + if (in_array($option['option_type'], ['date', 'date_time', 'time', 'file'])) { $product->setSkipCheckRequiredOption(false); - break; + $formattedOptions[$option['option_id']] = + $buyRequest->getDataByKey('options')[$option['option_id']]; + continue; } + $formattedOptions[$option['option_id']] = $option['option_value']; } if (!empty($formattedOptions)) { @@ -2123,17 +2125,4 @@ private function isAddressesAreEqual(Order $order) return $shippingData == $billingData; } - - /** - * Use Calendar on frontend or not - * - * @return bool - */ - private function useFrontendCalendar(): bool - { - return (bool)$this->_scopeConfig->getValue( - 'catalog/custom_options/use_calendar', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php new file mode 100644 index 0000000000000..af2cd504a1728 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Create; + +use Magento\Customer\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Escaper; +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Core\Version\View; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for reorder controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder + * @magentoAppArea adminhtml + */ +class ReorderTest extends AbstractBackendController +{ + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); + } + + /** + * Reorder with JS calendar options + * + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoDataFixture Magento/Sales/_files/order_with_date_time_option_product.php + * + * @return void + */ + public function testReorderAfterJSCalendarEnabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote('customer@example.com'); + $this->assertTrue(!empty($this->quote)); + } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/reorder'); + } + + /** + * Gets quote by reserved order id. + * + * @return \Magento\Quote\Api\Data\CartInterface + */ + private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('customer_email', $customerEmail) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php new file mode 100644 index 0000000000000..23fbeb94d2004 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; +$optionsDate = [ + [ + 'label' => 'date', + 'value' => 'Mar 5, 2020', + 'print_value' => 'Mar 5, 2020', + 'option_id' => '1', + 'option_type' => 'date', + 'option_value' => '2020-03-05 00:00:00', + 'custom_view' => '', + ] +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions([ + 'info_buyRequest' => $requestInfo, + 'options' => $optionsDate, +]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@example.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php new file mode 100644 index 0000000000000..0966f21645e3b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); From b0c27189e8e4930ee8dd2acd778c8a35dd4c9bf9 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 13 Oct 2020 16:07:33 +0300 Subject: [PATCH 076/195] MC-38269: [CLARIFICATION] [Magento Cloud] - Persistent Shopping cart Header Weelcome & Not you? --- .../view/frontend/templates/additional.phtml | 5 ++- .../web/js/view/additional-welcome.js | 2 +- .../Block/Header/AdditionalTest.php | 33 ++++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml index 0c19a1c5c8b05..61feeae04369d 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -5,9 +5,8 @@ */ ?> <script type="application/javascript"> - window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; - window.notYou = '<span> <a <?= /* @noEscape */ $block->getLinkAttributes()?>>' - + '<?= $block->escapeHtml(__("Not you?"));?></a></span>'; + window.persistent = <?=/* @noEscape */ $block->getConfig()?>; + window.notYouLink = '<?=/* @noEscape */ $block->getLinkAttributes()?>'; </script> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index fb57d311e35c3..2f5c42f090d18 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,7 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); - $(this).after(window.notYou); + $(this).after('<span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); }); } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php index c923a809441ba..42390f5303a94 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php @@ -6,41 +6,47 @@ namespace Magento\Persistent\Block\Header; +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as SessionHelper; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * @magentoDataFixture Magento/Persistent/_files/persistent.php */ -class AdditionalTest extends \PHPUnit\Framework\TestCase +class AdditionalTest extends TestCase { /** - * @var \Magento\Persistent\Block\Header\Additional + * @var Additional */ protected $_block; /** - * @var \Magento\Persistent\Helper\Session + * @var SessionHelper */ protected $_persistentSessionHelper; /** - * @var \Magento\Customer\Model\Session + * @var Session */ protected $_customerSession; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Persistent\Helper\Session $persistentSessionHelper */ - $this->_persistentSessionHelper = $this->_objectManager->create(\Magento\Persistent\Helper\Session::class); + /** @var Session $persistentSessionHelper */ + $this->_persistentSessionHelper = $this->_objectManager->create(Session::class); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + $this->_customerSession = $this->_objectManager->get(Session::class); - $this->_block = $this->_objectManager->create(\Magento\Persistent\Block\Header\Additional::class); + $this->_block = $this->_objectManager->create(Additional::class); } /** @@ -54,12 +60,7 @@ protected function setUp(): void public function testToHtml() { $this->_customerSession->loginById(1); - $translation = __('Not you?'); - - $this->assertStringContainsString( - '<a href="' . $this->_block->getHref() . '">' . $translation . '</a>', - $this->_block->toHtml() - ); + $this->assertStringContainsString($this->_block->getHref(), $this->_block->toHtml()); $this->_customerSession->logout(); } } From cd42c9d4a41512cf4a978120fcbeaac3d17d9e24 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 13 Oct 2020 22:57:16 +0300 Subject: [PATCH 077/195] MC-38074: Report - Products in Carts not following user roles scope --- .../Reports/Block/Adminhtml/Grid/Shopcart.php | 5 + .../Block/Adminhtml/Grid/ShopcartTest.php | 110 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php index afa0ce79aca6e..1d65dd5874c6e 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php @@ -28,6 +28,7 @@ class Shopcart extends \Magento\Backend\Block\Widget\Grid\Extended /** * StoreIds setter + * * @codeCoverageIgnore * * @param array $storeIds @@ -46,6 +47,10 @@ public function setStoreIds($storeIds) */ public function getCurrentCurrencyCode() { + if (empty($this->_storeIds)) { + $this->setStoreIds(array_keys($this->_storeManager->getStores())); + } + if ($this->_currentCurrencyCode === null) { reset($this->_storeIds); $this->_currentCurrencyCode = count( diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php new file mode 100644 index 0000000000000..25dcccdb1ef7a --- /dev/null +++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Unit\Block\Adminhtml\Grid; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Reports\Block\Adminhtml\Grid\Shopcart; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for class \Magento\Reports\Block\Adminhtml\Grid\Shopcart. + */ +class ShopcartTest extends TestCase +{ + /** + * @var Shopcart|MockObject + */ + private $model; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->storeManagerMock = $this->getMockForAbstractClass( + StoreManagerInterface::class, + [], + '', + true, + true, + true, + ['getStore'] + ); + + $this->model = $objectManager->getObject( + Shopcart::class, + ['_storeManager' => $this->storeManagerMock] + ); + } + + /** + * @param $storeIds + * + * @dataProvider getCurrentCurrencyCodeDataProvider + */ + public function testGetCurrentCurrencyCode($storeIds) + { + $storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + true, + true, + true, + ['getBaseCurrencyCode'] + ); + + $this->model->setStoreIds($storeIds); + + if ($storeIds) { + $expectedCurrencyCode = 'EUR'; + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with($storeIds[0]) + ->willReturn($storeMock); + $storeMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn($expectedCurrencyCode); + } else { + $expectedCurrencyCode = 'USD'; + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with(1) + ->willReturn($storeMock); + $this->storeManagerMock->expects($this->once()) + ->method('getStores') + ->willReturn([1 => $storeMock]); + $storeMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn($expectedCurrencyCode); + } + + $currencyCode = $this->model->getCurrentCurrencyCode(); + $this->assertEquals($expectedCurrencyCode, $currencyCode); + } + + /** + * DataProvider for testGetCurrentCurrencyCode. + * + * @return array + */ + public function getCurrentCurrencyCodeDataProvider() + { + return [ + [[]], + [[2]], + ]; + } +} From 6b39ef29532e070857bae7172daf444199db8dc2 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Thu, 15 Oct 2020 09:38:03 +0300 Subject: [PATCH 078/195] MC-38269: [CLARIFICATION] [Magento Cloud] - Persistent Shopping cart Header Weelcome & Not you? --- .../Persistent/view/frontend/web/js/view/additional-welcome.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index 2f5c42f090d18..8e69325860167 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,7 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); - $(this).after('<span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); + $(this).after(' <span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); }); } } From 15390c45ed1ffdfa48002157990013e45206099d Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Thu, 15 Oct 2020 11:26:34 -0500 Subject: [PATCH 079/195] MC-37460: Support by Magento CMS (#6202) * MC-37479: Support by Magento Content Design --- app/code/Magento/AwsS3/Driver/AwsS3.php | 145 +++++++++++-- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 51 +++++ .../Magento/Backup/Model/Fs/Collection.php | 16 +- .../Test/Unit/Model/Fs/CollectionTest.php | 18 +- .../Block/Adminhtml/Wysiwyg/Images/Tree.php | 5 +- .../Adminhtml/Wysiwyg/Directive.php | 19 +- .../Cms/Model/Wysiwyg/Images/Storage.php | 14 +- .../Wysiwyg/Images/Storage/Collection.php | 5 +- .../Adminhtml/Wysiwyg/DirectiveTest.php | 34 +-- .../Unit/Model/Wysiwyg/Images/StorageTest.php | 2 +- app/code/Magento/Cms/etc/di.xml | 16 +- .../Magento/Downloadable/Helper/Download.php | 37 ++-- .../Test/Unit/Helper/DownloadTest.php | 23 +- .../Magento/RemoteStorage/Plugin/Image.php | 204 ++++++++++++++++++ .../Test/Unit/Plugin/ImageTest.php | 174 +++++++++++++++ app/code/Magento/RemoteStorage/composer.json | 4 +- app/code/Magento/RemoteStorage/etc/di.xml | 18 ++ .../App/Filesystem/DirectoryResolver.php | 3 +- .../Unit/Filesystem/DirectoryResolverTest.php | 3 +- .../Framework/Data/Collection/Filesystem.php | 47 +++- lib/internal/Magento/Framework/File/Mime.php | 23 +- .../Framework/File/Test/Unit/MimeTest.php | 26 ++- .../Image/Adapter/AbstractAdapter.php | 30 ++- 23 files changed, 828 insertions(+), 89 deletions(-) create mode 100644 app/code/Magento/RemoteStorage/Plugin/Image.php create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 8a862a6812107..320e3f9c43a54 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -14,6 +14,8 @@ /** * Driver for AWS S3 IO operations. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class AwsS3 implements DriverInterface { @@ -173,10 +175,7 @@ public function filePutContents($path, $content, $mode = null, $context = null): */ public function readDirectoryRecursively($path = null): array { - return $this->adapter->listContents( - $this->normalizeRelativePath($path), - true - ); + return $this->readPath($path, true); } /** @@ -184,10 +183,7 @@ public function readDirectoryRecursively($path = null): array */ public function readDirectory($path): array { - return $this->adapter->listContents( - $this->normalizeRelativePath($path), - false - ); + return $this->readPath($path, false); } /** @@ -402,11 +398,11 @@ public function stat($path): array 'ctime' => 0, 'blksize' => 0, 'blocks' => 0, - 'size' => $metaInfo['size'], - 'type' => $metaInfo['type'], - 'mtime' => $metaInfo['timestamp'], + 'size' => $metaInfo['size'] ?? 0, + 'type' => $metaInfo['type'] ?? 0, + 'mtime' => $metaInfo['timestamp'] ?? 0, 'disposition' => null, - 'mimetype' => $metaInfo['mimetype'] + 'mimetype' => $metaInfo['mimetype'] ?? 0 ]; } @@ -415,7 +411,36 @@ public function stat($path): array */ public function search($pattern, $path): array { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')); + } + + /** + * Emulate php glob function for AWS S3 storage + * + * @param string $pattern + * @return array + * @throws FileSystemException + */ + private function glob(string $pattern): array + { + $directoryContent = []; + + $patternFound = preg_match('(\*|\?|\[.+\])', $pattern, $parentPattern, PREG_OFFSET_CAPTURE); + if ($patternFound) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $parentDirectory = \dirname(substr($pattern, 0, $parentPattern[0][1] + 1)); + $leftover = substr($pattern, $parentPattern[0][1]); + $index = strpos($leftover, '/'); + $searchPattern = $this->getSearchPattern($pattern, $parentPattern, $parentDirectory, $index); + + if ($this->isDirectory($parentDirectory . '/')) { + $directoryContent = $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); + } + } elseif ($this->isDirectory($pattern) || $this->isFile($pattern)) { + $directoryContent[] = $pattern; + } + + return $directoryContent; } /** @@ -630,4 +655,98 @@ private function getWarningMessage(): ?string return null; } + + /** + * Read directory by path and is recursive flag + * + * @param string $path + * @param bool $isRecursive + * @return array + */ + private function readPath(string $path, $isRecursive = false): array + { + $relativePath = $this->normalizeRelativePath($path); + $contentsList = $this->adapter->listContents( + $relativePath, + $isRecursive + ); + $itemsList = []; + foreach ($contentsList as $item) { + if (isset($item['path']) + && $item['path'] !== $relativePath + && strpos($item['path'], $relativePath) === 0) { + $itemsList[] = $item['path']; + } + } + + return $itemsList; + } + + /** + * Get search pattern for directory + * + * @param string $pattern + * @param array $parentPattern + * @param string $parentDirectory + * @param int|bool $index + * @return string + */ + private function getSearchPattern(string $pattern, array $parentPattern, string $parentDirectory, $index): string + { + $parentLength = \strlen($parentDirectory); + if ($index !== false) { + $searchPattern = substr( + $pattern, + $parentLength + 1, + $parentPattern[0][1] - $parentLength + $index - 1 + ); + } else { + $searchPattern = substr($pattern, $parentLength + 1); + } + + $replacement = [ + '/\*/' => '.*', + '/\?/' => '.', + '/\//' => '\/' + ]; + return preg_replace(array_keys($replacement), array_values($replacement), $searchPattern); + } + + /** + * Get directory content by given search pattern + * + * @param string $parentDirectory + * @param string $searchPattern + * @param string $leftover + * @param int|bool $index + * @return array + * @throws FileSystemException + */ + private function getDirectoryContent( + string $parentDirectory, + string $searchPattern, + string $leftover, + $index + ): array { + $items = $this->readDirectory($parentDirectory . '/'); + $directoryContent = []; + foreach ($items as $item) { + if (preg_match('/' . $searchPattern . '$/', $item) + // phpcs:ignore Magento2.Functions.DiscouragedFunction + && strpos(basename($item), '.') !== 0) { + if ($index === false || \strlen($leftover) === $index + 1) { + $directoryContent[] = $this->isDirectory($item) + ? rtrim($item, '/') . '/' + : $item; + } elseif (strlen($leftover) > $index + 1) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $directoryContent = array_merge( + $directoryContent, + $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)) + ); + } + } + } + return $directoryContent; + } } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index b3de684ed67dd..b70149e26225c 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -363,4 +363,55 @@ public function getRealPathSafetyDataProvider(): array ] ]; } + + /** + * @throws FileSystemException + */ + public function testSearchDirectory(): void + { + $expression = '/*'; + $path = 'path/'; + $subPaths = [ + ['path' => 'path/1'], + ['path' => 'path/2'] + ]; + $expectedResult = ['path/1', 'path/2']; + $this->adapterMock->expects(self::atLeastOnce())->method('has') + ->willReturnMap([ + [$path, true] + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('getMetadata') + ->willReturnMap([ + [$path, ['type' => AwsS3::TYPE_DIR]] + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('listContents')->with($path, false) + ->willReturn($subPaths); + self::assertEquals($expectedResult, $this->driver->search($expression, $path)); + } + + /** + * @throws FileSystemException + */ + public function testSearchFiles(): void + { + $expression = "/*"; + $path = 'path/'; + $subPaths = [ + ['path' => 'path/1.jpg'], + ['path' => 'path/2.png'] + ]; + $expectedResult = ['path/1.jpg', 'path/2.png']; + + $this->adapterMock->expects(self::atLeastOnce())->method('has') + ->willReturnMap([ + [$path, true], + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('getMetadata') + ->willReturnMap([ + [$path, ['type' => AwsS3::TYPE_DIR]], + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('listContents')->with($path, false) + ->willReturn($subPaths); + self::assertEquals($expectedResult, $this->driver->search($expression, $path)); + } } diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index b17c17f7074fb..94f555e4054e3 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -6,6 +6,8 @@ namespace Magento\Backup\Model\Fs; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Config\DocumentRoot; +use Magento\Framework\Filesystem\Directory\TargetDirectory; /** * Backup data collection @@ -40,20 +42,30 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem */ protected $_backup = null; + /** + * @var \Magento\Framework\Filesystem + */ + protected $_filesystem; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Magento\Backup\Helper\Data $backupData * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Backup\Model\Backup $backup + * @param TargetDirectory|null $targetDirectory + * @param DocumentRoot|null $documentRoot + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, \Magento\Backup\Helper\Data $backupData, \Magento\Framework\Filesystem $filesystem, - \Magento\Backup\Model\Backup $backup + \Magento\Backup\Model\Backup $backup, + TargetDirectory $targetDirectory = null, + DocumentRoot $documentRoot = null ) { $this->_backupData = $backupData; - parent::__construct($entityFactory); + parent::__construct($entityFactory, $targetDirectory, $documentRoot); $this->_filesystem = $filesystem; $this->_backup = $backup; diff --git a/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php b/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php index 69e2fcb6e1f25..cec0ccff70ce6 100644 --- a/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php +++ b/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php @@ -10,6 +10,7 @@ use Magento\Backup\Helper\Data; use Magento\Backup\Model\Fs\Collection; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; @@ -36,10 +37,23 @@ public function testConstructor() $directoryWrite->expects($this->any())->method('create')->with('backups'); $directoryWrite->expects($this->any())->method('getAbsolutePath')->with('backups'); - + $directoryWrite->expects($this->any())->method('isDirectory')->willReturn(true); + $targetDirectory = $this->getMockBuilder(TargetDirectory::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDirectoryWrite->expects($this->any())->method('isDirectory')->willReturn(true); + $targetDirectory->expects($this->any())->method('getDirectoryWrite')->willReturn($targetDirectoryWrite); $classObject = $helper->getObject( Collection::class, - ['filesystem' => $filesystem, 'backupData' => $backupData] + [ + 'filesystem' => $filesystem, + 'backupData' => $backupData, + 'directoryWrite' => $directoryWrite, + 'targetDirectory' => $targetDirectory + ] ); $this->assertNotNull($classObject); } diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php index 41e9358e160cf..c033e09ca8db0 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php @@ -58,6 +58,7 @@ public function __construct( * Json tree builder * * @return string + * @throws \Magento\Framework\Exception\ValidatorException */ public function getTreeJson() { @@ -75,8 +76,8 @@ public function getTreeJson() 'path' => substr($item->getFilename(), strlen($storageRoot)), 'cls' => 'folder', ]; - - $hasNestedDirectories = count(glob($item->getFilename() . '/*', GLOB_ONLYDIR)) > 0; + $nestedDirectories = $this->getMediaDirectory()->readRecursively($item->getFilename()); + $hasNestedDirectories = count($nestedDirectories) > 0; // if no nested directories inside dir, add 'leaf' state so that jstree hides dropdown arrow next to dir if (!$hasNestedDirectories) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php index a3370b2666264..5172ff8088bf8 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php @@ -13,6 +13,8 @@ use Magento\Cms\Model\Template\Filter; use Magento\Cms\Model\Wysiwyg\Config; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Image\Adapter\AdapterInterface; use Magento\Framework\Image\AdapterFactory; use Psr\Log\LoggerInterface; @@ -27,6 +29,7 @@ * Process template text for wysiwyg editor. * * Class Directive + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) usage of $this->file eliminated, but it's still there due to BC */ class Directive extends Action implements HttpGetActionInterface { @@ -70,8 +73,13 @@ class Directive extends Action implements HttpGetActionInterface /** * @var File + * @deprecated use $filesystem instead */ private $file; + /** + * @var Filesystem|null + */ + private $filesystem; /** * Constructor @@ -84,6 +92,7 @@ class Directive extends Action implements HttpGetActionInterface * @param Config|null $config * @param Filter|null $filter * @param File|null $file + * @param Filesystem|null $filesystem */ public function __construct( Context $context, @@ -93,7 +102,8 @@ public function __construct( LoggerInterface $logger = null, Config $config = null, Filter $filter = null, - File $file = null + File $file = null, + Filesystem $filesystem = null ) { parent::__construct($context); $this->urlDecoder = $urlDecoder; @@ -103,17 +113,21 @@ public function __construct( $this->config = $config ?: ObjectManager::getInstance()->get(Config::class); $this->filter = $filter ?: ObjectManager::getInstance()->get(Filter::class); $this->file = $file ?: ObjectManager::getInstance()->get(File::class); + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** * Template directives callback * * @return Raw + * @throws \Magento\Framework\Exception\FileSystemException */ public function execute() { $directive = $this->getRequest()->getParam('___directive'); $directive = $this->urlDecoder->decode($directive); + $image = null; + $resultRaw = null; try { /** @var Filter $filter */ $imagePath = $this->filter->filter($directive); @@ -141,7 +155,8 @@ public function execute() // To avoid issues with PNG images with alpha blending we return raw file // after validation as an image source instead of generating the new PNG image // with image adapter - $content = $this->file->fileGetContents($imagePath); + $content = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->getDriver() + ->fileGetContents($imagePath); $resultRaw->setHeader('Content-Type', $mimeType); $resultRaw->setContents($content); diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 0cc108e5bed8b..8b170ecdd5c04 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -369,9 +369,11 @@ public function getFilesCollection($path, $type = null) $item->setName($item->getBasename()); $item->setShortName($this->_cmsWysiwygImages->getShortFilename($item->getBasename())); $item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename()); - $itemStats = $this->file->stat($item->getFilename()); + $driver = $this->_directory->getDriver(); + $itemStats = $driver->stat($item->getFilename()); $item->setSize($itemStats['size']); - $item->setMimeType($this->mime->getMimeType($item->getFilename())); + $mimeType = $itemStats['mimetype'] ?? $this->mime->getMimeType($item->getFilename()); + $item->setMimeType($mimeType); if ($this->isImage($item->getBasename())) { $thumbUrl = $this->getThumbnailUrl($item->getFilename(), true); @@ -438,7 +440,7 @@ public function createDirectory($name, $path) $path = $this->_cmsWysiwygImages->getStorageRoot(); } - $newPath = $path . '/' . $name; + $newPath = rtrim($path, '/') . '/' . $name; $relativeNewPath = $this->_directory->getRelativePath($newPath); if ($this->_directory->isDirectory($relativeNewPath)) { throw new \Magento\Framework\Exception\LocalizedException( @@ -571,7 +573,7 @@ public function uploadFile($targetPath, $type = null) } // create thumbnail - $this->resizeFile($targetPath . '/' . $uploader->getUploadedFileName(), true); + $this->resizeFile($targetPath . '/' . ltrim($uploader->getUploadedFileName(), '/'), true); return $result; } @@ -759,7 +761,7 @@ public function getAllowedExtensions($type = null) */ public function getThumbnailRoot() { - return $this->_cmsWysiwygImages->getStorageRoot() . '/' . self::THUMBS_DIRECTORY_NAME; + return rtrim($this->_cmsWysiwygImages->getStorageRoot(), '/') . '/' . self::THUMBS_DIRECTORY_NAME; } /** @@ -844,7 +846,7 @@ protected function _sanitizePath($path) { return rtrim( preg_replace( - '~[/\\\]+~', + '~[/\\\]+(?<![htps?]://)~', '/', $this->_directory->getDriver()->getRealPathSafety( $this->_directory->getAbsolutePath($path) diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php index f66c0f6b06d91..ac60420713b26 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php @@ -41,10 +41,11 @@ public function __construct( */ protected function _generateRow($filename) { - $filename = preg_replace('~[/\\\]+~', '/', $filename); + $filename = preg_replace('~[/\\\]+(?<![htps?]://)~', '/', $filename); $path = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); return [ - 'filename' => $filename, + 'filename' => rtrim($filename, '/'), + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'basename' => basename($filename), 'mtime' => $path->stat($path->getRelativePath($filename))['mtime'] ]; diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php index 5791ecea4e4e3..70dd95521f040 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php @@ -15,7 +15,10 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Image\Adapter\AdapterInterface; use Magento\Framework\Image\AdapterFactory; use Magento\Framework\ObjectManagerInterface; @@ -78,11 +81,6 @@ class DirectiveTest extends TestCase */ protected $responseMock; - /** - * @var File|MockObject - */ - protected $fileMock; - /** * @var Config|MockObject */ @@ -103,6 +101,11 @@ class DirectiveTest extends TestCase */ protected $rawMock; + /** + * @var DriverInterface|MockObject + */ + private $driverMock; + protected function setUp(): void { $this->actionContextMock = $this->getMockBuilder(Context::class) @@ -146,10 +149,6 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['setHeader', 'setBody', 'sendResponse']) ->getMockForAbstractClass(); - $this->fileMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['fileGetContents']) - ->getMock(); $this->wysiwygConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); @@ -173,6 +172,17 @@ protected function setUp(): void $this->actionContextMock->expects($this->any()) ->method('getObjectManager') ->willReturn($this->objectManagerMock); + $this->driverMock = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWrite->expects($this->any())->method('getDriver')->willReturn($this->driverMock); + $filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($directoryWrite); $objectManager = new ObjectManager($this); $this->wysiwygDirective = $objectManager->getObject( @@ -185,7 +195,7 @@ protected function setUp(): void 'logger' => $this->loggerMock, 'config' => $this->wysiwygConfigMock, 'filter' => $this->templateFilterMock, - 'file' => $this->fileMock, + 'filesystem' => $filesystemMock ] ); } @@ -216,7 +226,7 @@ public function testExecute() $this->imageAdapterMock->expects($this->once()) ->method('getImage') ->willReturn($imageBody); - $this->fileMock->expects($this->once()) + $this->driverMock->expects($this->once()) ->method('fileGetContents') ->willReturn($imageBody); $this->rawFactoryMock->expects($this->any()) @@ -267,7 +277,7 @@ public function testExecuteException() $this->imageAdapterMock->expects($this->any()) ->method('getImage') ->willReturn($imageBody); - $this->fileMock->expects($this->once()) + $this->driverMock->expects($this->once()) ->method('fileGetContents') ->willReturn($imageBody); $this->loggerMock->expects($this->once()) diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index c2c748dcc7633..b03dbb8f0c888 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -494,7 +494,7 @@ public function testUploadFile() $targetPath = self::STORAGE_ROOT_DIR . $path; $fileName = 'image.gif'; $realPath = $targetPath . '/' . $fileName; - $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs' . $path; + $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '.thumbs' . $path; $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; $type = 'image'; $result = [ diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 7fc8268eea5e0..355848830dab6 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -57,35 +57,35 @@ <item name="exclude" xsi:type="array"> <item name="captcha" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+captcha[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+captcha[/\\]*$</item> </item> <item name="catalog/product" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+catalog[/\\]+product[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+catalog[/\\]+product[/\\]*$</item> </item> <item name="customer" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+customer[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+customer[/\\]*$</item> </item> <item name="downloadable" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+downloadable[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+downloadable[/\\]*$</item> </item> <item name="import" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+import[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+import[/\\]*$</item> </item> <item name="theme" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+theme[/\\]*$</item> </item> <item name="theme_customization" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme_customization[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+theme_customization[/\\]*$</item> </item> <item name="tmp" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+tmp[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+tmp[/\\]*$</item> </item> </item> <item name="include" xsi:type="array"/> diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 6b7db3af51195..1425f71f2fd8a 100644 --- a/app/code/Magento/Downloadable/Helper/Download.php +++ b/app/code/Magento/Downloadable/Helper/Download.php @@ -7,6 +7,8 @@ namespace Magento\Downloadable\Helper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Exception\LocalizedException as CoreException; @@ -18,12 +20,12 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper { /** - * Link type url + * Link type for url */ const LINK_TYPE_URL = 'url'; /** - * Link type file + * Link type for file */ const LINK_TYPE_FILE = 'file'; @@ -109,6 +111,11 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper */ protected $_session; + /** + * @var Mime + */ + private $mime; + /** * @param \Magento\Framework\App\Helper\Context $context * @param File $downloadableFile @@ -116,6 +123,7 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper * @param Filesystem $filesystem * @param \Magento\Framework\Session\SessionManagerInterface $session * @param Filesystem\File\ReadFactory $fileReadFactory + * @param Mime|null $mime */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -123,7 +131,8 @@ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb, \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Session\SessionManagerInterface $session, - \Magento\Framework\Filesystem\File\ReadFactory $fileReadFactory + \Magento\Framework\Filesystem\File\ReadFactory $fileReadFactory, + Mime $mime = null ) { parent::__construct($context); $this->_downloadableFile = $downloadableFile; @@ -131,6 +140,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->_session = $session; $this->fileReadFactory = $fileReadFactory; + $this->mime = $mime ?? ObjectManager::getInstance()->get(Mime::class); } /** @@ -148,6 +158,7 @@ protected function _getHandle() if ($this->_handle === null) { if ($this->_linkType == self::LINK_TYPE_URL) { $path = $this->_resourceFile; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $protocol = strtolower(parse_url($path, PHP_URL_SCHEME)); if ($protocol) { // Strip down protocol from path @@ -188,14 +199,8 @@ public function getContentType() { $this->_getHandle(); if ($this->_linkType === self::LINK_TYPE_FILE) { - if (function_exists('mime_content_type') - && ($contentType = mime_content_type( - $this->_workingDirectory->getAbsolutePath($this->_resourceFile) - )) - ) { - return $contentType; - } - return $this->_downloadableFile->getFileType($this->_resourceFile); + $absolutePath = $this->_workingDirectory->getAbsolutePath($this->_resourceFile); + return $this->mime->getMimeType($absolutePath); } if ($this->_linkType === self::LINK_TYPE_URL) { return (is_array($this->_handle->stat($this->_resourceFile)['type']) @@ -209,6 +214,8 @@ public function getContentType() * Return name of the file * * @return string + * phpcs:disable Magento2.Functions.DiscouragedFunction + * phpcs:disable Generic.PHP.NoSilencedErrors */ public function getFilename() { @@ -254,20 +261,21 @@ public function setResource($resourceFile, $linkType = self::LINK_TYPE_FILE) ); } } - + $this->_resourceFile = $resourceFile; - + /** * check header for urls */ if ($linkType === self::LINK_TYPE_URL) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $headers = array_change_key_case(get_headers($this->_resourceFile, 1), CASE_LOWER); if (isset($headers['location'])) { $this->_resourceFile = is_array($headers['location']) ? current($headers['location']) : $headers['location']; } } - + $this->_linkType = $linkType; return $this; } @@ -282,6 +290,7 @@ public function output() $handle = $this->_getHandle(); $this->_session->writeClose(); while (true == ($buffer = $handle->read(1024))) { + // phpcs:ignore Magento2.Security.LanguageConstruct echo $buffer; //@codingStandardsIgnoreLine } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php index 59de5b0139ff6..da89efac59fa8 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php @@ -10,6 +10,7 @@ use Magento\Downloadable\Helper\Download as DownloadHelper; use Magento\Downloadable\Helper\File as DownloadableFile; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface as DirReadInterface; use Magento\Framework\Filesystem\File\ReadFactory; @@ -62,6 +63,11 @@ class DownloadTest extends TestCase const URL = 'http://example.com'; + /** + * @var Mime|MockObject + */ + private $mime; + protected function setUp(): void { require_once __DIR__ . '/../_files/download_mock.php'; @@ -77,6 +83,7 @@ protected function setUp(): void SessionManagerInterface::class ); $this->fileReadFactory = $this->createMock(ReadFactory::class); + $this->mime = $this->createMock(Mime::class); $this->_helper = (new ObjectManager($this))->getObject( \Magento\Downloadable\Helper\Download::class, @@ -85,6 +92,7 @@ protected function setUp(): void 'filesystem' => $this->_filesystemMock, 'session' => $this->sessionManager, 'fileReadFactory' => $this->fileReadFactory, + 'mime' => $this->mime ] ); } @@ -132,8 +140,17 @@ public function testGetFileSizeNoFile() public function testGetContentType() { + $this->mime->expects( + self::once() + )->method( + 'getMimeType' + )->willReturn( + self::MIME_TYPE + ); $this->_setupFileMocks(); $this->_downloadableFileMock->expects($this->never())->method('getFileType'); + $this->_workingDirectoryMock->expects($this->once())->method('getAbsolutePath') + ->willReturn('/path/to/file.txt'); $this->assertEquals(self::MIME_TYPE, $this->_helper->getContentType()); } @@ -146,10 +163,10 @@ public function testGetContentTypeThroughHelper($functionExistsResult, $mimeCont self::$functionExists = $functionExistsResult; self::$mimeContentType = $mimeContentTypeResult; - $this->_downloadableFileMock->expects( - $this->once() + $this->mime->expects( + self::once() )->method( - 'getFileType' + 'getMimeType' )->willReturn( self::MIME_TYPE ); diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php new file mode 100644 index 0000000000000..8f554a3d8f8c3 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Image\Adapter\AbstractAdapter; +use Magento\RemoteStorage\Model\Config; +use Psr\Log\LoggerInterface; + +/** + * @see AbstractAdapter + */ +class Image +{ + /** + * @var Filesystem\Directory\WriteInterface + */ + private $tmpDirectoryWrite; + + /** + * @var Filesystem\Directory\WriteInterface + */ + private $targetDirectoryWrite; + + /** + * @var array + */ + private $tmpFiles = []; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @var File + */ + private $ioFile; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param File $ioFile + * @param TargetDirectory $targetDirectory + * @param Config $config + * @param LoggerInterface $logger + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct( + Filesystem $filesystem, + File $ioFile, + TargetDirectory $targetDirectory, + Config $config, + LoggerInterface $logger + ) { + $this->tmpDirectoryWrite = $filesystem->getDirectoryWrite(DirectoryList::TMP); + $this->targetDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); + $this->isEnabled = $config->isEnabled(); + $this->ioFile = $ioFile; + $this->logger = $logger; + } + + /** + * Copy file from remote server to tmp directory of Magento + * + * @param AbstractAdapter $subject + * @param string $filename + * @return array + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeOpen(AbstractAdapter $subject, $filename): array + { + if ($this->isEnabled) { + $filename = $this->copyFileToTmp($filename); + } + return [$filename]; + } + + /** + * Get filesystem tmp path for file and provide it to save() function + * + * @param AbstractAdapter $subject + * @param callable $proceed + * @param string|null $destination + * @param string|null $newName + * @return void + * @throws FileSystemException + */ + public function aroundSave( + AbstractAdapter $subject, + callable $proceed, + $destination = null, + $newName = null + ): void { + if ($this->isEnabled) { + $relativePath = $this->targetDirectoryWrite->getRelativePath($destination); + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath($relativePath); + + $proceed($tmpPath, $newName); + + $destination = $this->prepareDestination($subject, $destination, $newName); + $this->tmpDirectoryWrite->getDriver()->rename( + $tmpPath, + $destination, + $this->targetDirectoryWrite->getDriver() + ); + } else { + $proceed($destination, $newName); + } + } + + /** + * Remove created tmp files + */ + public function __destruct() + { + try { + foreach ($this->tmpFiles as $tmpFile) { + $this->tmpDirectoryWrite->delete($tmpFile); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + } + + /** + * Move files from storage to tmp folder + * + * @param string $filePath + * @return string + * @throws FileSystemException + */ + private function copyFileToTmp($filePath): string + { + $absolutePath = $this->targetDirectoryWrite->getAbsolutePath($filePath); + if ($this->targetDirectoryWrite->isFile($absolutePath)) { + $this->tmpDirectoryWrite->create(); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); + $this->storeTmpName($tmpPath); + $content = $this->targetDirectoryWrite->getDriver()->fileGetContents($filePath); + $filePath = $this->tmpDirectoryWrite->getDriver()->filePutContents($tmpPath, $content) + ? $tmpPath + : $filePath; + } + return $filePath; + } + + /** + * Store created tmp image path + * + * @param string $path + */ + private function storeTmpName(string $path): void + { + $this->tmpFiles[] = $path; + } + + /** + * Prepare destination path + * + * @param AbstractAdapter $image + * @param string|null $destination + * @param string|null $newName + * @return string + */ + private function prepareDestination( + AbstractAdapter $image, + string $destination = null, + string $newName = null + ): string { + if (empty($destination)) { + $destination = $image->getFileSrcPath(); + } elseif (empty($newName)) { + $info = $this->ioFile->getPathInfo($destination); + $newName = $info['basename']; + $destination = $info['dirname']; + } + + if (empty($newName)) { + $newFileName = $image->getFileSrcName(); + } else { + $newFileName = $newName; + } + return rtrim($destination, '/') . '/' . $newFileName; + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php new file mode 100644 index 0000000000000..4055422a8aa4e --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php @@ -0,0 +1,174 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\RemoteStorage\Test\Unit\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Image\Adapter\AbstractAdapter; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Plugin\Image; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class ImageTest extends TestCase +{ + /** + * @var File|MockObject + */ + private $ioFile; + + /** + * @var Image + */ + private $plugin; + + /** + * @var WriteInterface|MockObject + */ + private $tmpDirectoryWrite; + + /** + * @var WriteInterface|MockObject + */ + private $targetDirectoryWrite; + + /** + * @throws \Magento\Framework\Exception\FileSystemException + * @return void + */ + protected function setUp(): void + { + /** @var Filesystem|MockObject $filesystem */ + $filesystem = $this->getMockBuilder(Filesystem::class)->disableOriginalConstructor()->getMock(); + $this->ioFile = $this->getMockBuilder(File::class)->disableOriginalConstructor()->getMock(); + /** @var TargetDirectory|MockObject $targetDirectory */ + $targetDirectory = $this->getMockBuilder(TargetDirectory::class)->disableOriginalConstructor()->getMock(); + /** @var Config|MockObject $config */ + $config = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $config->expects(self::atLeastOnce())->method('isEnabled')->willReturn(true); + $this->tmpDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->targetDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor()->getMock(); + $filesystem->expects(self::atLeastOnce())->method('getDirectoryWrite')->with(DirectoryList::TMP) + ->willReturn($this->tmpDirectoryWrite); + $targetDirectory->expects(self::atLeastOnce())->method('getDirectoryWrite')->with(DirectoryList::ROOT) + ->willReturn($this->targetDirectoryWrite); + /** @var LoggerInterface|MockObject $logger */ + $logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->plugin = new Image( + $filesystem, + $this->ioFile, + $targetDirectory, + $config, + $logger + ); + } + + /** + * @dataProvider aroundSaveDataProvider + * @param string $destination + * @param string $newDestination + * @param string|null $newName + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function testAroundSaveWithNewName(string $destination, string $newDestination, ?string $newName): void + { + $tmpDestination = '/tmp/' . $destination; + /** @var AbstractAdapter $subject */ + $subject = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $proceed = function () { + }; + $targetDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getRelativePath') + ->willReturn($destination); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($targetDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') + ->willReturn($tmpDestination); + $driver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $driver->expects(self::atLeastOnce())->method('rename') + ->with($tmpDestination, $newDestination, $driver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver')->willReturn($driver); + $this->ioFile->expects(self::any())->method('getPathInfo')->with($destination) + ->willReturn(['dirname' => 'destination/', 'basename' => 'old_name.file']); + $this->plugin->aroundSave($subject, $proceed, $destination, $newName); + } + + /** + * @return array + */ + public function aroundSaveDataProvider(): array + { + return [ + 'with_new_name' => [ + 'destination' => 'destination/', + 'new_destination' => 'destination/new_name.file', + 'new_name' => 'new_name.file' + ], + 'with_old_name' => [ + 'destination' => 'destination/old_name.file', + 'new_destination' => 'destination/old_name.file', + 'new_name' => null + ] + ]; + } + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function testBeforeOpen(): void + { + /** @var AbstractAdapter $subject */ + $subject = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $filename = '/path/file_name.file'; + $absolutePath = 'absolute' . $filename; + $tmpAbsolutePath = '/var/www/magento2/tmp'; + $tmpFilePath = $tmpAbsolutePath . 'file_name.file'; + $content = 'Just a test'; + + $targetDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDriver->expects(self::atLeastOnce())->method('fileGetContents')->with($filename) + ->willReturn($content); + $tmpDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $tmpDriver->expects(self::atLeastOnce())->method('filePutContents')->with($tmpFilePath, $content) + ->willReturn(true); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath')->with($filename) + ->willReturn($absolutePath); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('isFile')->with($absolutePath) + ->willReturn(true); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($targetDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($tmpDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('create'); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') + ->willReturn($tmpAbsolutePath); + + self::assertEquals([$tmpFilePath], $this->plugin->beforeOpen($subject, $filename)); + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index 105b2b2b21a46..d82c47a7caf5e 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -7,7 +7,9 @@ }, "suggest": { "magento/module-backend": "*", - "magento/module-sitemap": "*" + "magento/module-sitemap": "*", + "magento/module-cms": "*", + "magento/module-downloadable": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index d9124326e65c2..586f07fc9ca83 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -74,4 +74,22 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> + <type name="Magento\Framework\Data\Collection\Filesystem"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\File\Mime"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> + <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> + </type> </config> diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php index 5ad3d888ffb57..c756fb43cf584 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php @@ -16,6 +16,7 @@ class DirectoryResolver { /** * @var DirectoryList + * @deprecated $this->filesystem->getDirectoryWrite() can be used for getting directory */ private $directoryList; @@ -51,7 +52,7 @@ public function validatePath($path, $directoryConfig = DirectoryList::MEDIA) { $directory = $this->filesystem->getDirectoryWrite($directoryConfig); $realPath = $directory->getDriver()->getRealPathSafety($path); - $root = $this->directoryList->getPath($directoryConfig); + $root = $directory->getAbsolutePath(); return strpos($realPath, $root) === 0; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php index 5549c34fa7701..2763dea8ef1e1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php @@ -75,8 +75,7 @@ public function testValidatePath(string $path, bool $expectedResult): void ->willReturnArgument(0); $this->filesystem->expects($this->atLeastOnce())->method('getDirectoryWrite')->with($directoryConfig) ->willReturn($directory); - $this->directoryList->expects($this->atLeastOnce())->method('getPath')->with($directoryConfig) - ->willReturn($rootPath); + $directory->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturn($rootPath); $this->assertEquals($expectedResult, $this->directoryResolver->validatePath($path, $directoryConfig)); } diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 6103a7df5bf0d..767cda60c0d35 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -7,7 +7,10 @@ namespace Magento\Framework\Data\Collection; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Data\Collection; +use Magento\Framework\Filesystem\Directory\TargetDirectory; /** * Filesystem items collection @@ -126,6 +129,32 @@ class Filesystem extends \Magento\Framework\Data\Collection */ protected $_collectedFiles = []; + /** + * @var TargetDirectory|null + */ + private $targetDirectory; + + /** + * @var DocumentRoot|null + */ + private $documentRoot; + + /** + * @param EntityFactoryInterface|null $_entityFactory + * @param TargetDirectory|null $targetDirectory + * @param DocumentRoot|null $documentRoot + */ + public function __construct( + EntityFactoryInterface $_entityFactory = null, + TargetDirectory $targetDirectory = null, + DocumentRoot $documentRoot = null + ) { + $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); + $this->targetDirectory = $targetDirectory ?? ObjectManager::getInstance()->get(TargetDirectory::class); + $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class); + parent::__construct($this->_entityFactory); + } + /** * Allowed dirs mask setter. Set empty to not filter. * @@ -208,7 +237,9 @@ public function setCollectRecursively($value) public function addTargetDir($value) { $value = (string)$value; - if (!is_dir($value)) { + $directory = $this->targetDirectory->getDirectoryWrite($this->documentRoot->getPath()); + + if (!$directory->isDirectory($value)) { // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Unable to set target directory.'); } @@ -235,17 +266,19 @@ public function setDirsFirst($value) * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _collectRecursive($dir) { + $directory = $this->targetDirectory->getDirectoryRead($this->documentRoot->getPath()); $collectedResult = []; if (!is_array($dir)) { $dir = [$dir]; } foreach ($dir as $folder) { - if ($nodes = glob($folder . '/*', GLOB_NOSORT)) { + if ($nodes = $directory->search('/*', $folder)) { foreach ($nodes as $node) { - $collectedResult[] = $node; + $collectedResult[] = $directory->getAbsolutePath($node); } } } @@ -254,7 +287,9 @@ protected function _collectRecursive($dir) } foreach ($collectedResult as $item) { - if (is_dir($item) && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item)))) { + if ($directory->isDirectory($item) + && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item))) + ) { if ($this->_collectDirs) { if ($this->_dirsFirst) { $this->_collectedDirs[] = $item; @@ -265,7 +300,7 @@ protected function _collectRecursive($dir) if ($this->_collectRecursively) { $this->_collectRecursive($item); } - } elseif ($this->_collectFiles && is_file( + } elseif ($this->_collectFiles && $directory->isFile( $item ) && (!$this->_allowedFilesMask || preg_match( $this->_allowedFilesMask, @@ -369,7 +404,7 @@ private function _generateAndFilterAndSort($attributeName) * * @param array $a * @param array $b - * @return int + * @return int|void */ protected function _usort($a, $b) { diff --git a/lib/internal/Magento/Framework/File/Mime.php b/lib/internal/Magento/Framework/File/Mime.php index e0b22e4c944d9..fe23969f32ce3 100644 --- a/lib/internal/Magento/Framework/File/Mime.php +++ b/lib/internal/Magento/Framework/File/Mime.php @@ -6,6 +6,9 @@ namespace Magento\Framework\File; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; + /** * Utility for mime type retrieval */ @@ -90,6 +93,20 @@ class Mime 'inode/x-empty', ]; + /** + * @var Filesystem + */ + private $filesystem; + + /** + * Mime constructor. + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem = null) + { + $this->filesystem = $filesystem; + } + /** * Get mime type of a file * @@ -99,14 +116,16 @@ class Mime */ public function getMimeType($file) { - if (!file_exists($file)) { + $directoryRead = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); + $fileExistsLocally = file_exists($file); + if (!$fileExistsLocally && !$directoryRead->isExist($file)) { throw new \InvalidArgumentException("File '$file' doesn't exist"); } $result = null; $extension = $this->getFileExtension($file); - if (function_exists('mime_content_type')) { + if (function_exists('mime_content_type') && $fileExistsLocally) { $result = $this->getNativeMimeType($file); } else { $imageInfo = getimagesize($file); diff --git a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php index 7a54a7966b500..ff70f0fb9b0c9 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php @@ -7,7 +7,11 @@ namespace Magento\Framework\File\Test\Unit; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -20,15 +24,29 @@ class MimeTest extends TestCase */ private $object; + /** + * @var ReadInterface|MockObject + */ + private $readInterface; + /** * @inheritDoc */ protected function setUp(): void { - $this->object = new Mime(); + $this->readInterface = $this->getMockBuilder(ReadInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Filesystem|MockObject $filesystem */ + $filesystem = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $filesystem->expects(self::any())->method('getDirectoryRead')->with(DirectoryList::ROOT) + ->willReturn($this->readInterface); + $this->object = new Mime($filesystem); } - public function testGetMimeTypeNonexistentFileException() + public function testGetMimeTypeNonexistentFileException(): void { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); @@ -42,10 +60,10 @@ public function testGetMimeTypeNonexistentFileException() * * @dataProvider getMimeTypeDataProvider */ - public function testGetMimeType($file, $expectedType) + public function testGetMimeType($file, $expectedType): void { $actualType = $this->object->getMimeType($file); - $this->assertSame($expectedType, $actualType); + self::assertSame($expectedType, $actualType); } /** diff --git a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php index 8b983809e643f..dc34fb3fcddc9 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php +++ b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php @@ -675,12 +675,10 @@ protected function _prepareDestination($destination = null, $newName = null) { if (empty($destination)) { $destination = $this->_fileSrcPath; - } else { - if (empty($newName)) { - $info = pathinfo((string) $destination); - $newName = $info['basename']; - $destination = $info['dirname']; - } + } elseif (empty($newName)) { + $info = pathinfo((string) $destination); + $newName = $info['basename']; + $destination = $info['dirname']; } if (empty($newName)) { @@ -751,4 +749,24 @@ public function validateUploadFile($filePath) return $this->getImageType() !== null; } + + /** + * Get file source path + * + * @return string + */ + public function getFileSrcPath(): string + { + return $this->_fileSrcPath ?? ''; + } + + /** + * Get file source name + * + * @return string + */ + public function getFileSrcName(): string + { + return $this->_fileSrcName ?? ''; + } } From f65a214005960a9cbfae0e5907549a5f298cf851 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 15 Oct 2020 15:38:11 -0500 Subject: [PATCH 080/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Model/Indexer/Product/Category/Action/Rows.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 861f7c9c1c50e..7441580e5a21a 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -18,6 +18,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Indexer\Model\WorkingStateProvider; /** * Category rows indexer. @@ -48,6 +49,11 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private $indexerRegistry; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + /** * @param ResourceConnection $resource * @param StoreManagerInterface $storeManager @@ -57,6 +63,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry + * @param WorkingStateProvider|null $workingStateProvider */ public function __construct( ResourceConnection $resource, @@ -66,12 +73,15 @@ public function __construct( MetadataPool $metadataPool = null, CacheContext $cacheContext = null, EventManagerInterface $eventManager = null, - IndexerRegistry $indexerRegistry = null + IndexerRegistry $indexerRegistry = null, + ?WorkingStateProvider $workingStateProvider = null ) { parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); + $this->workingStateProvider = $workingStateProvider ?: + ObjectManager::getInstance()->get(WorkingStateProvider::class); } /** @@ -90,7 +100,7 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); - $workingState = $indexer->isWorking(); + $workingState = $this->workingStateProvider->isWorking($indexer->getId()); $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); From 011b3e256d7f0af64be5a573128e9cf3b2475723 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 15 Oct 2020 16:17:11 -0500 Subject: [PATCH 081/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Product/Category/Action/Rows.php | 5 +- .../Product/Category/Action/RowsTest.php | 250 ++++++++++++++++++ 2 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 7441580e5a21a..f2965b7b68479 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -18,6 +18,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Indexer\Model\WorkingStateProvider; /** @@ -60,6 +61,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param Config $config * @param QueryGenerator|null $queryGenerator * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry @@ -71,12 +73,13 @@ public function __construct( Config $config, QueryGenerator $queryGenerator = null, MetadataPool $metadataPool = null, + ?TableMaintainer $tableMaintainer = null, CacheContext $cacheContext = null, EventManagerInterface $eventManager = null, IndexerRegistry $indexerRegistry = null, ?WorkingStateProvider $workingStateProvider = null ) { - parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool, $tableMaintainer); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php new file mode 100644 index 0000000000000..d9792433dfe6f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php @@ -0,0 +1,250 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Category\Action; + +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Product\Category\Action\Rows; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Indexer\Model\WorkingStateProvider; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Indexer\IndexerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for Rows action + */ +class RowsTest extends TestCase +{ + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + + /** + * @var ResourceConnection|MockObject + */ + private $resource; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Config|MockObject + */ + private $config; + + /** + * @var QueryGenerator|MockObject + */ + private $queryGenerator; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var CacheContext|MockObject + */ + private $cacheContext; + + /** + * @var EventManagerInterface|MockObject + */ + private $eventManager; + + /** + * @var IndexerRegistry|MockObject + */ + private $indexerRegistry; + + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainer; + + /** + * @var IndexerInterface|MockObject + */ + private $indexer; + + /** + * @var AdapterInterface|MockObject + */ + private $connection; + + /** + * @var Select|MockObject + */ + private $select; + + /** + * @var Rows + */ + private $rowsModel; + + protected function setUp() : void + { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resource = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->resource->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connection); + $this->select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->select->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('distinct') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('group') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinLeft') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('columns') + ->willReturnSelf(); + $this->connection->expects($this->any()) + ->method('select') + ->willReturn($this->select); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->queryGenerator = $this->getMockBuilder(QueryGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cacheContext = $this->getMockBuilder(CacheContext::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eventManager = $this->getMockBuilder(EventManagerInterface::class) + ->getMockForAbstractClass(); + $this->indexerRegistry = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexer = $this->getMockBuilder(IndexerInterface::class) + ->getMockForAbstractClass(); + $this->tableMaintainer = $this->getMockBuilder(TableMaintainer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->rowsModel = new Rows( + $this->resource, + $this->storeManager, + $this->config, + $this->queryGenerator, + $this->metadataPool, + $this->tableMaintainer, + $this->cacheContext, + $this->eventManager, + $this->indexerRegistry, + $this->workingStateProvider + ); + } + + public function testExecuteWithIndexerWorking() : void + { + $categoryId = '1'; + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any()) + ->method('getRootCategoryId') + ->willReturn($categoryId); + $store->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $attribute = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->config->expects($this->any()) + ->method('getAttribute') + ->willReturn($attribute); + + $table = $this->getMockBuilder(Table::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any()) + ->method('newTable') + ->willReturn($table); + + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $this->connection->expects($this->any()) + ->method('fetchAll') + ->willReturn([]); + $this->connection->expects($this->any()) + ->method('fetchCol') + ->willReturn([]); + + $this->connection->expects($this->any()) + ->method('fetchOne') + ->willReturn($categoryId); + $this->indexerRegistry->expects($this->any()) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexer->expects($this->any()) + ->method('getId') + ->willReturn(CategoryProductIndexer::INDEXER_ID); + $this->workingStateProvider->expects($this->any()) + ->method('isWorking') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn(true); + $this->storeManager->expects($this->any()) + ->method('getStores') + ->willReturn([$store]); + + $this->connection->expects($this->once()) + ->method('delete'); + + $result = $this->rowsModel->execute([1, 2, 3]); + $this->assertInstanceOf(Rows::class, $result); + } +} From 703c117a6c91cb9bbae5489b0ac8f3481280a3c4 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Fri, 16 Oct 2020 13:14:59 -0500 Subject: [PATCH 082/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Catalog/Model/Indexer/Category/Product/Action/Rows.php | 1 + .../Catalog/Model/Indexer/Product/Category/Action/Rows.php | 1 + .../Unit/Model/Indexer/Category/Product/Action/RowsTest.php | 2 ++ .../Unit/Model/Indexer/Product/Category/Action/RowsTest.php | 2 ++ 4 files changed, 6 insertions(+) diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index 3ff7b519ac132..f05687c53edb5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -65,6 +65,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry * @param WorkingStateProvider|null $workingStateProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Preserve compatibility with the parent class */ public function __construct( ResourceConnection $resource, diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index f2965b7b68479..9c17304e12613 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -66,6 +66,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry * @param WorkingStateProvider|null $workingStateProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Preserve compatibility with the parent class */ public function __construct( ResourceConnection $resource, diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php index 8ec2d3fd1afe7..48cd2de65945e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php @@ -31,6 +31,8 @@ /** * Test for Rows action + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with tested class */ class RowsTest extends TestCase { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php index d9792433dfe6f..6750ccd60775c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php @@ -31,6 +31,8 @@ /** * Test for Rows action + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with tested class */ class RowsTest extends TestCase { From 52a288b24c774db2c82b56d07e10d7116fe4d1ef Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Sat, 17 Oct 2020 18:17:57 +0300 Subject: [PATCH 083/195] revert changes --- .../Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml index 99c60eba67854..4f0e9bb000a27 100644 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml @@ -22,13 +22,14 @@ <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> </actionGroup> + <reloadPage stepKey="pageReload"/> </before> <after> <magentoCLI command="config:set admin/usage/enabled 0" stepKey="disableAdminUsageTracking"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="ReloadPageActionGroup" stepKey="pageReload"/> + <waitForPageLoad stepKey="waitForPageReloaded"/> <seeInPageSource html="var adminAnalyticsMetadata =" stepKey="seeInPageSource"/> </test> </tests> From 6a46a2e9c7b55c23f0f541df39c545049e630ca0 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Mon, 19 Oct 2020 15:17:00 -0500 Subject: [PATCH 084/195] MC-37933: Product Export File Does Not Show In Admin - fixed --- .../Model/Export/RowCustomizer.php | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php index daa874e829e54..f24b27389215a 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php @@ -82,31 +82,38 @@ public function prepareData($collection, $productIds): void ->addAttributeToSelect('samples_title'); // set global scope during export $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); - foreach ($collection as $product) { - $productLinks = $this->linkRepository->getLinksByProduct($product); - $productSamples = $this->sampleRepository->getSamplesByProduct($product); - $this->downloadableData[$product->getId()] = []; - $linksData = []; - $samplesData = []; - foreach ($productLinks as $linkId => $link) { - $linkData = $link->getData(); - $linkData['group_title'] = $product->getData('links_title'); - $linksData[$linkId] = $this->optionRowToCellString($linkData); - } - foreach ($productSamples as $sampleId => $sample) { - $sampleData = $sample->getData(); - $sampleData['group_title'] = $product->getData('samples_title'); - $samplesData[$sampleId] = $this->optionRowToCellString($sampleData); + + $collection->setPageSize(100); + $pages = $collection->getLastPageNumber(); + for ($pageNum = 1; $pageNum <= $pages; $pageNum++) { + $collection->setCurPage($pageNum); + foreach ($collection as $product) { + $productLinks = $this->linkRepository->getLinksByProduct($product); + $productSamples = $this->sampleRepository->getSamplesByProduct($product); + $this->downloadableData[$product->getId()] = []; + $linksData = []; + $samplesData = []; + foreach ($productLinks as $linkId => $link) { + $linkData = $link->getData(); + $linkData['group_title'] = $product->getData('links_title'); + $linksData[$linkId] = $this->optionRowToCellString($linkData); + } + foreach ($productSamples as $sampleId => $sample) { + $sampleData = $sample->getData(); + $sampleData['group_title'] = $product->getData('samples_title'); + $samplesData[$sampleId] = $this->optionRowToCellString($sampleData); + } + $this->downloadableData[$product->getId()] = [ + Downloadable::COL_DOWNLOADABLE_LINKS => implode( + ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, + $linksData + ), + Downloadable::COL_DOWNLOADABLE_SAMPLES => implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + $samplesData + )]; } - $this->downloadableData[$product->getId()] = [ - Downloadable::COL_DOWNLOADABLE_LINKS => implode( - ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, - $linksData - ), - Downloadable::COL_DOWNLOADABLE_SAMPLES => implode( - Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, - $samplesData - )]; + $collection->clear(); } } From 5448cbc77b74f0badac9dd9c187caf83e49ad4c3 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 20 Oct 2020 09:49:52 +0300 Subject: [PATCH 085/195] MC-38476: Can not register customer with correct postal code for Argentina --- app/code/Magento/Directory/etc/zip_codes.xml | 1 + .../Country/Postcode/Config/ReaderTest.php | 20 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Directory/etc/zip_codes.xml b/app/code/Magento/Directory/etc/zip_codes.xml index 14d250656d28c..de6c064815d7a 100644 --- a/app/code/Magento/Directory/etc/zip_codes.xml +++ b/app/code/Magento/Directory/etc/zip_codes.xml @@ -19,6 +19,7 @@ <zip countryCode="AR"> <codes> <code id="pattern_1" active="true" example="1234">^[0-9]{4}$</code> + <code id="pattern_2" active="true" example="A1234BCD">^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$</code> </codes> </zip> <zip countryCode="AM"> diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php index 740afcda11386..9146535ed5181 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php @@ -6,18 +6,20 @@ namespace Magento\Directory\Model\Country\Postcode\Config; -class ReaderTest extends \PHPUnit\Framework\TestCase +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class ReaderTest extends TestCase { /** - * @var \Magento\Directory\Model\Country\Postcode\Config\Reader + * @var Reader */ private $reader; protected function setUp(): void { - $this->reader = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Directory\Model\Country\Postcode\Config\Reader::class - ); + $this->reader = Bootstrap::getObjectManager() + ->create(Reader::class); } public function testRead() @@ -39,5 +41,13 @@ public function testRead() $this->assertEquals('test1', $result['NL_NEW']['pattern_1']['example']); $this->assertEquals('^[0-2]{4}[A-Z]{2}$', $result['NL_NEW']['pattern_1']['pattern']); + + $this->assertArrayHasKey('AR', $result); + $this->assertArrayHasKey('pattern_1', $result['AR']); + $this->assertArrayHasKey('pattern_2', $result['AR']); + $this->assertEquals('1234', $result['AR']['pattern_1']['example']); + $this->assertEquals('^[0-9]{4}$', $result['AR']['pattern_1']['pattern']); + $this->assertEquals('A1234BCD', $result['AR']['pattern_2']['example']); + $this->assertEquals('^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$', $result['AR']['pattern_2']['pattern']); } } From fa2e1bcd49a28e2ed9498c1b1cd6f400d9005df6 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 20 Oct 2020 12:38:21 +0300 Subject: [PATCH 086/195] MC-38306: [Cloud] Adding new disabled products to Magento flushes categories cache --- .../Category/Product/PositionResolver.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 44bf153f83697..f7c0ea608123e 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -49,4 +49,30 @@ public function getPositions(int $categoryId): array return array_flip($connection->fetchCol($select)); } + + /** + * Get category product positions + * + * @param int $categoryId + * @return int + */ + public function getLastPosition(int $categoryId): int + { + $connection = $this->getConnection(); + + $select = $connection->select()->from( + ['cpe' => $this->getTable('catalog_product_entity')], + ['position' => new \Zend_Db_Expr('MAX(position)')] + )->joinLeft( + ['ccp' => $this->getTable('catalog_category_product')], + 'ccp.product_id=cpe.entity_id' + )->where( + 'ccp.category_id = ?', + $categoryId + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC + ); + + return (int)$connection->fetchOne($select); + } } From 46239525dde56e3fed909a895f04d34c8d86c4de Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 20 Oct 2020 15:06:01 +0300 Subject: [PATCH 087/195] MC-38342: [B2B] Multiple identical warning messages are displayed when adding an unconfigured Product with Customizable Options to a Requisition List from a Category page --- .../Model/Product/Type/AbstractType.php | 3 +- .../Model/Product/Type/AbstractTypeTest.php | 204 +++++++++++------- 2 files changed, 129 insertions(+), 78 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index eb4a71cb90a8c..bfdafacb11aad 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; @@ -620,7 +621,7 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p } } if (count($results) > 0) { - throw new LocalizedException(__(implode("\n", $results))); + throw new LocalizedException(__(implode("\n", array_unique($results)))); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php index c72e7e0e1d078..4cd9d74e58418 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php @@ -3,41 +3,60 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AbstractTypeTest extends \PHPUnit\Framework\TestCase +class AbstractTypeTest extends TestCase { /** - * @var \Magento\Catalog\Model\Product\Type\AbstractType + * @var AbstractType */ protected $_model; protected function setUp(): void { - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Api\ProductRepositoryInterface::class + $productRepository = Bootstrap::getObjectManager()->get( + ProductRepositoryInterface::class ); - $catalogProductOption = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product\Option::class + $catalogProductOption = Bootstrap::getObjectManager()->get( + Option::class ); - $catalogProductType = $this->createMock(\Magento\Catalog\Model\Product\Type::class); - $eventManager = $this->createPartialMock(\Magento\Framework\Event\ManagerInterface::class, ['dispatch']); - $fileStorageDb = $this->createMock(\Magento\MediaStorage\Helper\File\Storage\Database::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $registry = $this->createMock(\Magento\Framework\Registry::class); - $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $serializer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Serialize\Serializer\Json::class + $catalogProductType = $this->createMock(Type::class); + $eventManager = $this->createPartialMock(ManagerInterface::class, ['dispatch']); + $fileStorageDb = $this->createMock(Database::class); + $filesystem = $this->createMock(Filesystem::class); + $registry = $this->createMock(Registry::class); + $logger = $this->createMock(LoggerInterface::class); + $serializer = Bootstrap::getObjectManager()->get( + Json::class ); $this->_model = $this->getMockForAbstractClass( - \Magento\Catalog\Model\Product\Type\AbstractType::class, + AbstractType::class, [ $catalogProductOption, - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class), + Bootstrap::getObjectManager()->get(Config::class), $catalogProductType, $eventManager, $fileStorageDb, @@ -53,7 +72,7 @@ protected function setUp(): void public function testGetRelationInfo() { $info = $this->_model->getRelationInfo(); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $info); + $this->assertInstanceOf(DataObject::class, $info); $this->assertNotSame($info, $this->_model->getRelationInfo()); } @@ -72,8 +91,8 @@ public function testGetParentIdsByChild() */ public function testGetSetAttributes() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -85,7 +104,7 @@ public function testGetSetAttributes() $this->assertArrayHasKey('name', $attributes); $isTypeExists = false; foreach ($attributes as $attribute) { - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute); $applyTo = $attribute->getApplyTo(); if (count($applyTo) > 0 && !in_array('simple', $applyTo)) { $isTypeExists = true; @@ -97,9 +116,9 @@ public function testGetSetAttributes() public function testAttributesCompare() { - $attribute[1] = new \Magento\Framework\DataObject(['group_sort_path' => 1, 'sort_path' => 10]); - $attribute[2] = new \Magento\Framework\DataObject(['group_sort_path' => 1, 'sort_path' => 5]); - $attribute[3] = new \Magento\Framework\DataObject(['group_sort_path' => 2, 'sort_path' => 10]); + $attribute[1] = new DataObject(['group_sort_path' => 1, 'sort_path' => 10]); + $attribute[2] = new DataObject(['group_sort_path' => 1, 'sort_path' => 5]); + $attribute[3] = new DataObject(['group_sort_path' => 2, 'sort_path' => 10]); $this->assertEquals(1, $this->_model->attributesCompare($attribute[1], $attribute[2])); $this->assertEquals(-1, $this->_model->attributesCompare($attribute[2], $attribute[1])); $this->assertEquals(-1, $this->_model->attributesCompare($attribute[1], $attribute[3])); @@ -110,9 +129,9 @@ public function testAttributesCompare() public function testGetAttributeById() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class )->load( 1 ); @@ -120,8 +139,8 @@ public function testGetAttributeById() $this->assertNull($this->_model->getAttributeById(-1, $product)); $this->assertNull($this->_model->getAttributeById(null, $product)); - $sku = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Eav\Model\Config::class + $sku = Bootstrap::getObjectManager()->get( + Config::class )->getAttribute( 'catalog_product', 'sku' @@ -140,8 +159,8 @@ public function testGetAttributeById() */ public function testIsVirtual() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertFalse($this->_model->isVirtual($product)); } @@ -151,8 +170,8 @@ public function testIsVirtual() */ public function testIsSalable() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertTrue($this->_model->isSalable($product)); @@ -169,20 +188,20 @@ public function testIsSalable() */ public function testPrepareForCart() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->load(10); // fixture $this->assertEmpty($product->getCustomOption('info_buyRequest')); $requestData = ['qty' => 5]; - $result = $this->_model->prepareForCart(new \Magento\Framework\DataObject($requestData), $product); + $result = $this->_model->prepareForCart(new DataObject($requestData), $product); $this->assertArrayHasKey(0, $result); $this->assertSame($product, $result[0]); $buyRequest = $product->getCustomOption('info_buyRequest'); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $buyRequest); + $this->assertInstanceOf(DataObject::class, $buyRequest); $this->assertEquals($product->getId(), $buyRequest->getProductId()); $this->assertSame($product, $buyRequest->getProduct()); $this->assertEquals(json_encode($requestData), $buyRequest->getValue()); @@ -193,15 +212,15 @@ public function testPrepareForCart() */ public function testPrepareForCartOptionsException() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture $this->assertStringContainsString( "The product's required option(s) weren't entered. Make sure the options are entered and try again.", - $this->_model->prepareForCart(new \Magento\Framework\DataObject(), $product) + $this->_model->prepareForCart(new DataObject(), $product) ); } @@ -215,9 +234,9 @@ public function testGetSpecifyOptionMessage() public function testCheckProductBuyState() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->setSkipCheckRequiredOption('_'); $this->_model->checkProductBuyState($product); @@ -228,10 +247,10 @@ public function testCheckProductBuyState() */ public function testCheckProductBuyStateException() { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -243,9 +262,9 @@ public function testCheckProductBuyStateException() */ public function testGetOrderOptions() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertEquals([], $this->_model->getOrderOptions($product)); @@ -283,8 +302,8 @@ public function testGetOrderOptions() */ public function testBeforeSave() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -299,8 +318,8 @@ public function testBeforeSave() */ public function testGetSku() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -312,9 +331,9 @@ public function testGetSku() */ public function testGetOptionSku() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertEmpty($this->_model->getOptionSku($product)); @@ -336,7 +355,7 @@ public function testGetOptionSku() public function testGetWeight() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertEmpty($this->_model->getWeight($product)); $product->setWeight('value'); $this->assertEquals('value', $this->_model->getWeight($product)); @@ -346,16 +365,16 @@ public function testHasOptions() { $this->markTestIncomplete('Bug MAGE-2814'); - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertFalse($this->_model->hasOptions($product)); - $product = new \Magento\Framework\DataObject(['has_options' => true]); + $product = new DataObject(['has_options' => true]); $this->assertTrue($this->_model->hasOptions($product)); } public function testHasRequiredOptions() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertFalse($this->_model->hasRequiredOptions($product)); $product->setRequiredOptions(1); $this->assertTrue($this->_model->hasRequiredOptions($product)); @@ -363,7 +382,7 @@ public function testHasRequiredOptions() public function testGetSetStoreFilter() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertNull($this->_model->getStoreFilter($product)); $store = new \StdClass(); $this->_model->setStoreFilter($store, $product); @@ -374,8 +393,8 @@ public function testGetForceChildItemQtyChanges() { $this->assertFalse( $this->_model->getForceChildItemQtyChanges( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -387,8 +406,8 @@ public function testPrepareQuoteItemQty() 3.0, $this->_model->prepareQuoteItemQty( 3, - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -396,12 +415,12 @@ public function testPrepareQuoteItemQty() public function testAssignProductToOption() { - $product = new \Magento\Framework\DataObject(); - $option = new \Magento\Framework\DataObject(); + $product = new DataObject(); + $option = new DataObject(); $this->_model->assignProductToOption($product, $option, $product); $this->assertSame($product, $option->getProduct()); - $option = new \Magento\Framework\DataObject(); + $option = new DataObject(); $this->_model->assignProductToOption(null, $option, $product); $this->assertSame($product, $option->getProduct()); } @@ -415,8 +434,8 @@ public function testSetConfig() { $this->assertFalse( $this->_model->isComposite( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -425,8 +444,8 @@ public function testSetConfig() $this->_model->setConfig($config); $this->assertTrue( $this->_model->isComposite( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -438,8 +457,8 @@ public function testSetConfig() */ public function testGetSearchableData() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->load(1); // fixture @@ -467,10 +486,41 @@ public function testProcessBuyRequest() public function testCheckProductConfiguration() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); - $buyRequest = new \Magento\Framework\DataObject(['qty' => 5]); + $buyRequest = new DataObject(['qty' => 5]); $this->_model->checkProductConfiguration($product, $buyRequest); } + + /** + * Test that only one exception appears instead of multiple identical exceptions + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void + */ + public function testPrepareOptions(): void + { + $exceptionMessage = + "The product's required option(s) weren't entered. Make sure the options are entered and try again."; + $product = Bootstrap::getObjectManager()->create( + Product::class + ); + $product->load(1); + $buyRequest = new DataObject(['product' => 1]); + $method = new \ReflectionMethod( + AbstractType::class, + '_prepareOptions' + ); + $method->setAccessible(true); + $exceptionIsThrown = false; + try { + $method->invoke($this->_model, $buyRequest, $product, 'full'); + } catch (LocalizedException $exception) { + $this->assertEquals($exceptionMessage, $exception->getMessage()); + $exceptionIsThrown = true; + } + $this->assertTrue($exceptionIsThrown); + } } From 5543a9f31b13bddba7918f200636c8dc0935a319 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 20 Oct 2020 19:15:36 +0300 Subject: [PATCH 088/195] MC-38306: [Cloud] Adding new disabled products to Magento flushes categories cache --- .../Catalog/Model/Category/Product/PositionResolver.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index f7c0ea608123e..320a253a9a1dd 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -51,18 +51,18 @@ public function getPositions(int $categoryId): array } /** - * Get category product positions + * Get category product minimum position * * @param int $categoryId * @return int */ - public function getLastPosition(int $categoryId): int + public function getMinPosition(int $categoryId): int { $connection = $this->getConnection(); $select = $connection->select()->from( ['cpe' => $this->getTable('catalog_product_entity')], - ['position' => new \Zend_Db_Expr('MAX(position)')] + ['position' => new \Zend_Db_Expr('MIN(position)')] )->joinLeft( ['ccp' => $this->getTable('catalog_category_product')], 'ccp.product_id=cpe.entity_id' From 79c51b6848754bf293cf126e4bfb6263152decd3 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Tue, 20 Oct 2020 17:51:16 -0500 Subject: [PATCH 089/195] MC-37933: Product Export File Does Not Show In Admin - added unit test --- .../Unit/Model/Export/RowCustomizerTest.php | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php new file mode 100644 index 0000000000000..27f3d62bd90ec --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableImportExport\Test\Unit\Model\Export; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Downloadable\Model\LinkRepository; +use Magento\Downloadable\Model\SampleRepository; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class RowCustomizerTest for export RowCustomizer + */ +class RowCustomizerTest extends TestCase +{ + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var LinkRepository|MockObject + */ + private $linkRepositoryMock; + + /** + * @var SampleRepository|MockObject + */ + private $sampleRepositoryMock; + + /** + * @var \Magento\DownloadableImportExport\Model\Export\RowCustomizer + */ + private $model; + + /** + * Setup + * + * @return void + */ + protected function setUp(): void + { + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->linkRepositoryMock = $this->getMockBuilder(LinkRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sampleRepositoryMock = $this->getMockBuilder(SampleRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + \Magento\DownloadableImportExport\Model\Export\RowCustomizer::class, + [ + 'storeManager' => $this->storeManagerMock, + 'linkRepository' => $this->linkRepositoryMock, + 'sampleRepository' => $this->sampleRepositoryMock, + ] + ); + } + + /** + * Test Prepare configurable data for export + */ + public function testPrepareData() + { + $product1 = $this->getMockBuilder(ProductInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $product1->expects($this->any()) + ->method('getId') + ->willReturn(1); + $product2 = $this->getMockBuilder(ProductInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $product2->expects($this->any()) + ->method('getId') + ->willReturn(2); + $collection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $collection->expects($this->atLeastOnce()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$product1, $product2])); + + $collection->expects($this->exactly(2)) + ->method('addAttributeToFilter') + ->willReturnSelf(); + $collection->expects($this->exactly(2)) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $collection->expects($this->once()) + ->method('setPageSize') + ->willReturnSelf(); + $collection->expects($this->once()) + ->method('getLastPageNumber') + ->willReturn(1); + $collection->expects($this->atLeastOnce()) + ->method('setCurPage') + ->willReturnSelf(); + $collection->expects($this->once()) + ->method('clear') + ->willReturnSelf(); + + $this->linkRepositoryMock->expects($this->exactly(2)) + ->method('getLinksByProduct') + ->will($this->returnValue([])); + $this->sampleRepositoryMock->expects($this->exactly(2)) + ->method('getSamplesByProduct') + ->will($this->returnValue([])); + + $this->model->prepareData($collection, []); + } +} From 320fae74b0edb284d1d430155cf2b798dd1e0a27 Mon Sep 17 00:00:00 2001 From: Serhii Voloshkov <serhii.voloshkov@transoftgroup.com> Date: Wed, 21 Oct 2020 11:00:35 +0300 Subject: [PATCH 090/195] MC-36768: Custom attribute value created in integration tests fixtures product not appears in elasticsearch --- .../Indexer/Fulltext/Action/DataProvider.php | 96 ++++--- .../Indexer/Fulltext/Plugin/Attribute.php | 18 +- .../Indexer/Fulltext/Plugin/AttributeTest.php | 35 ++- .../FieldMapper/Product/AttributeProvider.php | 20 +- .../Plugin/Category/Product/Attribute.php | 12 +- .../Catalog/ProductSearchAggregationsTest.php | 6 +- .../GraphQl/Catalog/ProductSearchTest.php | 25 +- .../Swatches/ProductSwatchDataTest.php | 10 +- .../Magento/Catalog/Helper/ProductTest.php | 147 ++++++----- .../Magento/Catalog/_files/categories.php | 141 +++++----- ...th_custom_attribute_layered_navigation.php | 27 +- .../_files/product_boolean_attribute.php | 85 +++--- ...ucts_with_layered_navigation_attribute.php | 242 +++++++++--------- ..._layered_navigation_attribute_rollback.php | 58 +++-- ...th_layered_navigation_custom_attribute.php | 218 ++++++++-------- .../_files/configurable_attribute.php | 106 ++++---- .../_files/configurable_products.php | 68 ++--- .../Indexer/_files/reindex_all_invalid.php | 13 + .../order_configurable_product_rollback.php | 1 + ..._with_visual_swatch_attribute_rollback.php | 7 +- ...e_with_different_options_type_rollback.php | 17 +- 21 files changed, 758 insertions(+), 594 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 8c4690f044764..3a67025230430 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -6,10 +6,24 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\ResourceModel\EngineInterface; +use Magento\CatalogSearch\Model\ResourceModel\EngineProvider; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadata; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Zend_Db; /** * Catalog search full test search data provider. @@ -24,7 +38,7 @@ class DataProvider /** * Searchable attributes cache * - * @var \Magento\Eav\Model\Entity\Attribute[] + * @var Attribute[] */ private $searchableAttributes; @@ -50,40 +64,40 @@ class DataProvider private $productEmulators = []; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory + * @var CollectionFactory */ private $productAttributeCollectionFactory; /** * Eav config * - * @var \Magento\Eav\Model\Config + * @var Config */ private $eavConfig; /** * Catalog product type * - * @var \Magento\Catalog\Model\Product\Type + * @var Type */ private $catalogProductType; /** * Core event manager proxy * - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ private $eventManager; /** * Store manager * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; /** - * @var \Magento\CatalogSearch\Model\ResourceModel\EngineInterface + * @var EngineInterface */ private $engine; @@ -93,12 +107,12 @@ class DataProvider private $resource; /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface + * @var AdapterInterface */ private $connection; /** - * @var \Magento\Framework\EntityManager\EntityMetadata + * @var EntityMetadata */ private $metadata; @@ -126,24 +140,24 @@ class DataProvider /** * @param ResourceConnection $resource - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttributeCollectionFactory - * @param \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param Type $catalogProductType + * @param Config $eavConfig + * @param CollectionFactory $prodAttributeCollectionFactory + * @param EngineProvider $engineProvider + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param MetadataPool $metadataPool * @param int $antiGapMultiplier */ public function __construct( ResourceConnection $resource, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttributeCollectionFactory, - \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, + Type $catalogProductType, + Config $eavConfig, + CollectionFactory $prodAttributeCollectionFactory, + EngineProvider $engineProvider, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + MetadataPool $metadataPool, int $antiGapMultiplier = 5 ) { $this->resource = $resource; @@ -224,7 +238,7 @@ private function getSelectForSearchableProducts( $batch ) { $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId(); - $lastProductId = (int) $lastProductId; + $lastProductId = (int)$lastProductId; $select = $this->connection->select() ->useStraightJoin(true) @@ -242,7 +256,7 @@ private function getSelectForSearchableProducts( $this->joinAttribute($select, 'status', $storeId, [Status::STATUS_ENABLED]); if ($productIds !== null) { - $select->where('e.entity_id IN (?)', $productIds, \Zend_Db::INT_TYPE); + $select->where('e.entity_id IN (?)', $productIds, Zend_Db::INT_TYPE); } $select->where('e.entity_id > ?', $lastProductId); $select->order('e.entity_id'); @@ -308,14 +322,17 @@ private function joinAttribute(Select $select, $attributeCode, $storeId, array $ */ public function getSearchableAttributes($backendType = null) { + /** TODO: Remove this block in the next minor release and add a new public method instead */ + if ($this->eavConfig->getEntityType(Product::ENTITY)->getNeedRefreshSearchAttributesList()) { + $this->clearSearchableAttributesList(); + } if (null === $this->searchableAttributes) { $this->searchableAttributes = []; - /** @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection $productAttributes */ $productAttributes = $this->productAttributeCollectionFactory->create(); $productAttributes->addToIndexFilter(true); - /** @var \Magento\Eav\Model\Entity\Attribute[] $attributes */ + /** @var Attribute[] $attributes */ $attributes = $productAttributes->getItems(); /** @deprecated */ @@ -329,7 +346,7 @@ public function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -355,6 +372,18 @@ public function getSearchableAttributes($backendType = null) return $this->searchableAttributes; } + /** + * Remove searchable attributes list. + * + * @return void + */ + private function clearSearchableAttributesList(): void + { + $this->searchableAttributes = null; + $this->searchableAttributesByBackendType = []; + $this->eavConfig->getEntityType(Product::ENTITY)->unsNeedRefreshSearchAttributesList(); + } + /** * Retrieve searchable attribute by Id or code * @@ -369,7 +398,7 @@ public function getSearchableAttribute($attribute) return $attributes[$attribute]; } - return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(Product::ENTITY, $attribute); } /** @@ -386,6 +415,7 @@ private function unifyField($field, $backendType = 'varchar') } else { $expr = $field; } + return $expr; } @@ -411,7 +441,7 @@ public function getProductAttributes($storeId, array $productIds, array $attribu )->where( 'cpe.entity_id IN (?)', $productIds, - \Zend_Db::INT_TYPE + Zend_Db::INT_TYPE ) ); foreach ($attributeTypes as $backendType => $attributeIds) { @@ -479,6 +509,7 @@ private function getProductTypeInstance($typeId) $this->productTypes[$typeId] = $this->catalogProductType->factory($productEmulator); } + return $this->productTypes[$typeId]; } @@ -513,6 +544,7 @@ public function getProductChildIds($productId, $typeId) if ($relation->getWhere() !== null) { $select->where($relation->getWhere()); } + return $this->connection->fetchCol($select); } @@ -528,10 +560,11 @@ public function getProductChildIds($productId, $typeId) private function getProductEmulator($typeId) { if (!isset($this->productEmulators[$typeId])) { - $productEmulator = new \Magento\Framework\DataObject(); + $productEmulator = new DataObject(); $productEmulator->setTypeId($typeId); $this->productEmulators[$typeId] = $productEmulator; } + return $this->productEmulators[$typeId]; } @@ -660,6 +693,7 @@ function ($value) { $attributeOptionValue .= $this->attributeOptions[$optionKey][$attrValueId] . ' '; } } + return empty($attributeOptionValue) ? null : trim($attributeOptionValue); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php index 7b5d43ece922d..3f0046b918f28 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php @@ -5,12 +5,15 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; use Magento\Framework\Search\Request\Config; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\Eav\Model\Config as EavConfig; /** * Catalog search indexer plugin for catalog attribute. @@ -37,16 +40,24 @@ class Attribute extends AbstractPlugin */ private $saveIsNew; + /** + * @var EavConfig + */ + private $eavConfig; + /** * @param IndexerRegistry $indexerRegistry * @param Config $config + * @param EavConfig $eavConfig */ public function __construct( IndexerRegistry $indexerRegistry, - Config $config + Config $config, + EavConfig $eavConfig ) { parent::__construct($indexerRegistry); $this->config = $config; + $this->eavConfig = $eavConfig; } /** @@ -84,6 +95,11 @@ public function afterSave( } if ($this->saveIsNew || $this->saveNeedInvalidation) { $this->config->reset(); + /** + * TODO: Remove this in next minor release and use public method instead. + * @see DataProvider::getSearchableAttributes + */ + $this->eavConfig->getEntityType(Product::ENTITY)->setNeedRefreshSearchAttributesList(true); } return $result; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php index befe462184af6..4d8a7de391356 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php @@ -7,8 +7,10 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Indexer\Fulltext\Plugin; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext; use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute; +use Magento\Eav\Model\Config as EavConfig; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Search\Request\Config; @@ -16,6 +18,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Unit tests for @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute. + */ class AttributeTest extends TestCase { /** @@ -53,6 +58,14 @@ class AttributeTest extends TestCase */ private $config; + /** + * @var EavConfig + */ + private $eavConfig; + + /** + * @inheridoc + */ protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -78,11 +91,16 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['reset']) ->getMock(); + $this->eavConfig = $this->createPartialMock( + EavConfig::class, + ['getEntityType'] + ); $this->model = $this->objectManager->getObject( Attribute::class, [ 'indexerRegistry' => $this->indexerRegistryMock, - 'config' => $this->config + 'config' => $this->config, + 'eavConfig' => $this->eavConfig ] ); } @@ -123,21 +141,26 @@ public function testAfterSaveWithInvalidation(bool $saveNeedInvalidation, bool $ [ 'indexerRegistry' => $this->indexerRegistryMock, 'config' => $this->config, + 'eavConfig' => $this->eavConfig, 'saveNeedInvalidation' => $saveNeedInvalidation, 'saveIsNew' => $saveIsNew, ] ); + if ($saveIsNew || $saveNeedInvalidation) { + $this->config->expects($this->once()) + ->method('reset'); + $catalogProductEntity = $this->createMock(Product::class); + $this->eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(Product::ENTITY) + ->willReturn($catalogProductEntity); + } if ($saveNeedInvalidation) { $this->indexerMock->expects($this->once())->method('invalidate'); $this->prepareIndexer(); } - if ($saveIsNew || $saveNeedInvalidation) { - $this->config->expects($this->once()) - ->method('reset'); - } - $this->assertEquals( $this->subjectMock, $model->afterSave($this->subjectMock, $this->subjectMock) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php index 89c98d29ae03e..75636991e7ee6 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute; +use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; /** @@ -20,7 +21,7 @@ class AttributeProvider /** * Object Manager instance * - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -49,13 +50,13 @@ class AttributeProvider /** * Factory constructor * - * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param Config $eavConfig * @param LoggerInterface $logger * @param string $instanceName */ public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, + ObjectManagerInterface $objectManager, Config $eavConfig, LoggerInterface $logger, $instanceName = AttributeAdapter::class @@ -87,4 +88,17 @@ public function getByAttributeCode(string $attributeCode): AttributeAdapter return $this->cachedPool[$attributeCode]; } + + /** + * Remove attribute from cache by code. + * + * @param string $attributeCode + * @return void + */ + public function removeAttributeCacheByCode(string $attributeCode): void + { + if (isset($this->cachedPool[$attributeCode])) { + unset($this->cachedPool[$attributeCode]); + } + } } diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php index 53f036a3b8e38..e15d91148b8ce 100644 --- a/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php +++ b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Indexer\IndexerHandler as ElasticsearchIndexerHandler; use Magento\Framework\Indexer\DimensionProviderInterface; use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; @@ -41,6 +42,11 @@ class Attribute */ private $indexerHandlerFactory; + /** + * @var AttributeProvider + */ + private $attributeProvider; + /** * @var bool */ @@ -56,17 +62,20 @@ class Attribute * @param Processor $indexerProcessor * @param DimensionProviderInterface $dimensionProvider * @param IndexerHandlerFactory $indexerHandlerFactory + * @param AttributeProvider $attributeProvider */ public function __construct( Config $config, Processor $indexerProcessor, DimensionProviderInterface $dimensionProvider, - IndexerHandlerFactory $indexerHandlerFactory + IndexerHandlerFactory $indexerHandlerFactory, + AttributeProvider $attributeProvider ) { $this->config = $config; $this->indexerProcessor = $indexerProcessor; $this->dimensionProvider = $dimensionProvider; $this->indexerHandlerFactory = $indexerHandlerFactory; + $this->attributeProvider = $attributeProvider; } /** @@ -82,6 +91,7 @@ public function afterSave( AttributeResourceModel $subject, AttributeResourceModel $result ): AttributeResourceModel { + $this->attributeProvider->removeAttributeCacheByCode($this->attributeCode); $indexer = $this->indexerProcessor->getIndexer(); if ($this->isNewObject && !$indexer->isScheduled() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 9dbd902f1714e..b8e1587fcad71 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -17,10 +17,7 @@ class ProductSearchAggregationsTest extends GraphQlAbstract */ public function testAggregationBooleanAttribute() { - $this->markTestSkipped( - 'MC-22184: Elasticsearch returns incorrect aggregation options for booleans' - . 'MC-36768: Custom attribute not appears in elasticsearch' - ); + $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $skus= '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"'; $query = <<<QUERY @@ -64,7 +61,6 @@ function ($a) { $this->assertEquals('boolean_attribute', $booleanAggregation['attribute_code']); $this->assertContainsEquals(['label' => '1', 'value'=> '1', 'count' => '3'], $booleanAggregation['options']); - $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $this->assertEquals(2, $booleanAggregation['count']); $this->assertCount(2, $booleanAggregation['options']); $this->assertContainsEquals(['label' => '0', 'value'=> '0', 'count' => '2'], $booleanAggregation['options']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index f755a1a1e0282..2355eb281ac38 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -19,6 +19,7 @@ use Magento\Eav\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\ObjectManager; @@ -67,14 +68,11 @@ public function testFilterForNonExistingCategory() * Verify that layered navigation filters and aggregations are correct for product query * * Filter products by an array of skus - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterLn() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); $query = <<<QUERY { products ( @@ -149,14 +147,12 @@ private function compareFilterNames(array $a, array $b) * Layered navigation for Configurable products with out of stock options * Two configurable products each having two variations and one of the child products of one Configurable set to OOS * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testLayeredNavigationForConfigurableProducts() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; @@ -256,12 +252,11 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi * Filter products by custom attribute of dropdown type and filterTypeInput eq * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterProductsByDropDownCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); @@ -455,12 +450,11 @@ private function getDefaultAttributeOptionValue(string $attributeCode): string * Full text search for Products and then filter the results by custom attribute (default sort is relevance) * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSearchAndFilterByCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); $attribute_code = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); @@ -603,18 +597,19 @@ public function testSearchAndFilterByCustomAttribute() * Filter by category and custom attribute * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterByCategoryIdAndCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); - $categoryId = 13; + /** @var GetCategoryByName $getCategoryByName */ + $getCategoryByName = Bootstrap::getObjectManager()->get(GetCategoryByName::class); + $category = $getCategoryByName->execute('Category 1.2'); $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); $query = <<<QUERY { products(filter:{ - category_id : {eq:"{$categoryId}"} + category_id : {eq:"{$category->getId()}"} second_test_configurable: {eq: "{$optionValue}"} }, pageSize: 3 @@ -2368,7 +2363,6 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() /** * Verify that invalid current page return an error * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testInvalidCurrentPage() @@ -2399,7 +2393,6 @@ public function testInvalidCurrentPage() /** * Verify that invalid page size returns an error. * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testInvalidPageSize() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php index 1514613987b40..ae34ea31f0d51 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php @@ -32,8 +32,7 @@ protected function setUp(): void } /** - * @magentoApiDataFixture Magento/Swatches/_files/text_swatch_attribute.php - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoApiDataFixture Magento/Swatches/_files/configurable_product_text_swatch_attribute.php */ public function testTextSwatchDataValues() { @@ -68,14 +67,15 @@ public function testTextSwatchDataValues() $option = $product['configurable_options'][0]; $this->assertArrayHasKey('values', $option); $length = count($option['values']); + $swatchData = ['Swatch 1', 'Swatch 2', 'Swatch 3']; for ($i = 0; $i < $length; $i++) { - $this->assertEquals('option ' . ($i + 1), $option['values'][$i]['swatch_data']['value']); + $swatchValue = $option['values'][$i]['swatch_data']['value']; + $this->assertContains($swatchValue, $swatchData); } } /** - * @magentoApiDataFixture Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoApiDataFixture Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute.php */ public function testVisualSwatchDataValues() { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php index 98f623e5f193b..4c0f74f009330 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php @@ -5,34 +5,69 @@ */ namespace Magento\Catalog\Helper; -class ProductTest extends \PHPUnit\Framework\TestCase +use Exception; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Session; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppIsolation enabled + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductTest extends TestCase { /** - * @var \Magento\Catalog\Helper\Product + * @var ProductHelper */ protected $helper; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductInterfaceFactory + */ + private $productFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheridoc + */ protected function setUp(): void { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\App\State::class) - ->setAreaCode('frontend'); - $this->helper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Helper\Product::class - ); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(ProductHelper::class); + /** @var ProductInterfaceFactory $productInterfaceFactory */ + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->registry = $this->objectManager->get(Registry::class); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php - * @magentoAppIsolation enabled */ public function testGetProductUrl() { @@ -46,20 +81,16 @@ public function testGetProductUrl() public function testGetPrice() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $product->setPrice(49.95); $this->assertEquals(49.95, $this->helper->getPrice($product)); } public function testGetFinalPrice() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $product->setPrice(49.95); $product->setFinalPrice(49.95); $this->assertEquals(49.95, $this->helper->getFinalPrice($product)); @@ -67,10 +98,8 @@ public function testGetFinalPrice() public function testGetImageUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/image.jpg', $this->helper->getImageUrl($product)); $product->setImage('test_image.png'); @@ -79,10 +108,8 @@ public function testGetImageUrl() public function testGetSmallImageUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/small_image.jpg', $this->helper->getSmallImageUrl($product)); $product->setSmallImage('test_image.png'); @@ -91,10 +118,8 @@ public function testGetSmallImageUrl() public function testGetThumbnailUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/thumbnail.jpg', $this->helper->getThumbnailUrl($product)); $product->setThumbnail('test_image.png'); $this->assertStringEndsWith('/test_image.png', $this->helper->getThumbnailUrl($product)); @@ -102,26 +127,20 @@ public function testGetThumbnailUrl() public function testGetEmailToFriendUrl() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->productFactory->create(); $product->setId(100); - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); + $category = $this->objectManager->create(CategoryInterfaceFactory::class)->create(); $category->setId(10); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); + $this->registry->register('current_category', $category); try { $this->assertStringEndsWith( 'sendfriend/product/send/id/100/cat_id/10/', $this->helper->getEmailToFriendUrl($product) ); - $objectManager->get(\Magento\Framework\Registry::class)->unregister('current_category'); - } catch (\Exception $e) { - $objectManager->get(\Magento\Framework\Registry::class)->unregister('current_category'); + $this->registry->unregister('current_category'); + } catch (Exception $e) { + $this->registry->unregister('current_category'); throw $e; } } @@ -137,17 +156,15 @@ public function testGetStatuses() public function testCanShow() { // non-visible or disabled - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertFalse($this->helper->canShow($product)); $existingProduct = $this->productRepository->get('simple'); // enabled and visible $product->setId($existingProduct->getId()); - $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); - $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH); + $product->setStatus(Status::STATUS_ENABLED); + $product->setVisibility(Visibility::VISIBILITY_BOTH); $this->assertTrue($this->helper->canShow($product)); $this->assertTrue($this->helper->canShow((int)$product->getId())); @@ -193,39 +210,27 @@ public function testGetAttributeSourceModelByInputType() } /** - * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation enabled - * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/categories.php */ public function testInitProduct() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $objectManager->get(\Magento\Catalog\Model\Session::class)->setLastVisitedCategoryId(2); + $this->objectManager->get(Session::class)->setLastVisitedCategoryId(2); $product = $this->productRepository->get('simple'); $this->helper->initProduct($product->getId(), 'view'); - $this->assertInstanceOf( - \Magento\Catalog\Model\Product::class, - $objectManager->get(\Magento\Framework\Registry::class)->registry('current_product') - ); - $this->assertInstanceOf( - \Magento\Catalog\Model\Category::class, - $objectManager->get(\Magento\Framework\Registry::class)->registry('current_category') - ); + $this->assertInstanceOf(Product::class, $this->registry->registry('current_product')); + $this->assertInstanceOf(Category::class, $this->registry->registry('current_category')); } public function testPrepareProductOptions() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $buyRequest = new \Magento\Framework\DataObject(['qty' => 100, 'options' => ['option' => 'value']]); + /** @var $product Product */ + $product = $this->productFactory->create(); + $buyRequest = new DataObject(['qty' => 100, 'options' => ['option' => 'value']]); $this->helper->prepareProductOptions($product, $buyRequest); $result = $product->getPreconfiguredValues(); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $result); + $this->assertInstanceOf(DataObject::class, $result); $this->assertEquals(100, $result->getQty()); $this->assertEquals(['option' => 'value'], $result->getOptions()); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index 4255d7d3c98e5..9b743542b8573 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -3,29 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) - ->getEntityType('catalog_product') - ->getDefaultAttributeSetId(); +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\CategoryLinkRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Eav\Model\Config; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; -$productRepository = $objectManager->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class -); +$objectManager = Bootstrap::getObjectManager(); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); +$rootCategoryId = $baseWebsite->getDefaultStore()->getRootCategoryId(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +$defaultAttributeSet = $objectManager->get(Config::class)->getEntityType(Product::ENTITY)->getDefaultAttributeSetId(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$categoryFactory = $objectManager->get(CategoryInterfaceFactory::class); $categoryLinkRepository = $objectManager->create( - \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + CategoryLinkRepositoryInterface::class, [ - 'productRepository' => $productRepository + 'productRepository' => $productRepository, ] ); /** @var Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ -$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +$categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); $reflectionClass = new \ReflectionClass(get_class($categoryLinkManagement)); $properties = [ 'productRepository' => $productRepository, - 'categoryLinkRepository' => $categoryLinkRepository + 'categoryLinkRepository' => $categoryLinkRepository, ]; foreach ($properties as $key => $value) { if ($reflectionClass->hasProperty($key)) { @@ -39,7 +58,7 @@ * After installation system has two categories: root one with ID:1 and Default category with ID:2 */ /** @var $category \Magento\Catalog\Model\Category */ -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(3) ->setName('Category 1') @@ -52,7 +71,7 @@ ->setPosition(1) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(4) ->setName('Category 1.1') @@ -67,7 +86,7 @@ ->setDescription('Category 1.1 description.') ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(5) ->setName('Category 1.1.1') @@ -83,7 +102,7 @@ ->setDescription('This is the description for Category 1.1.1') ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(6) ->setName('Category 2') @@ -96,7 +115,7 @@ ->setPosition(2) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(7) ->setName('Movable') @@ -109,7 +128,7 @@ ->setPosition(3) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(8) ->setName('Inactive') @@ -122,7 +141,7 @@ ->setPosition(4) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(9) ->setName('Movable Position 1') @@ -135,7 +154,7 @@ ->setPosition(5) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(10) ->setName('Movable Position 2') @@ -148,7 +167,7 @@ ->setPosition(6) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(11) ->setName('Movable Position 3') @@ -161,7 +180,7 @@ ->setPosition(7) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(12) ->setName('Category 12') @@ -174,7 +193,7 @@ ->setPosition(8) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(13) ->setName('Category 1.2') @@ -189,84 +208,86 @@ ->setPosition(2) ->save(); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product') ->setSku('simple') ->setPrice(10) ->setWeight(18) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple1 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple1->getSku(), [2, 3, 4, 13] ); -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Two') ->setSku('12345') // SKU intentionally contains digits only ->setPrice(45.67) ->setWeight(56) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple2 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple2->getSku(), [5, 4] ); -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Not Visible On Storefront') ->setSku('simple-3') ->setPrice(15) ->setWeight(2) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED); + +$simple3 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple3->getSku(), [10, 11, 12] ); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Three') ->setSku('simple-4') ->setPrice(10) ->setWeight(18) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple4 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple4->getSku(), [10, 11, 12, 13] ); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php index c2c3782c8cd23..6737aef1eb487 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -5,29 +5,26 @@ */ declare(strict_types=1); -use Magento\TestFramework\Workaround\Override\Fixture\Resolver; - -Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); - +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); -/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$objectManager = Bootstrap::getObjectManager(); -$eavConfig->clear(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $attribute Attribute */ +$attribute = $attributeRepository->get('test_configurable'); $attribute->setIsSearchable(1) - ->setIsVisibleInAdvancedSearch(1) - ->setIsFilterable(true) - ->setIsFilterableInSearch(true) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) ->setIsVisibleOnFront(1); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); - CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php index 57b918fb5e663..6f81d6b659996 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php @@ -5,51 +5,56 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\Entity\Attribute\Source\Boolean; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); -/** @var Attribute $attribute */ -$attribute = $objectManager->create(Attribute::class); -/** @var $installer CategorySetup */ -$installer = $objectManager->create(CategorySetup::class); -try { - $attributeRepository->get(CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, 'boolean_attribute'); -} catch (NoSuchEntityException $e) { - $attribute->setData( - [ - 'attribute_code' => 'boolean_attribute', - 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, - 'is_global' => 0, - 'is_user_defined' => 1, - 'frontend_input' => 'boolean', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 0, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 0, - 'frontend_label' => ['Boolean Attribute'], - 'backend_type' => 'int', - 'source_model' => Boolean::class - ] - ); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var $installer CategorySetup */ +$installer = $objectManager->get(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); - $attributeRepository->save($attribute); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'boolean_attribute', + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'boolean', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Boolean Attribute'], + 'backend_type' => 'int', + 'source_model' => Boolean::class + ] +); +$attribute = $attributeRepository->save($attributeModel); - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); -} +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 29812aa942ab5..3bc3fef56e32e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -5,125 +5,135 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Indexer\Model\Indexer; +use Magento\Indexer\Model\Indexer\Collection; +use Magento\Msrp\Model\Product\Attribute\Source\Type as SourceType; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); - +$objectManager = Bootstrap::getObjectManager(); + +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); + +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] +); +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); +CacheCleaner::cleanAll(); $eavConfig->clear(); -/** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); - -if (!$attribute->getId()) { - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute->setData( - [ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default' => ['option_0'] - ] - ); - - $attributeRepository->save($attribute); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); - CacheCleaner::cleanAll(); -} - -$eavConfig->clear(); - -/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setId(10) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product1') ->setSku('simple1') ->setTaxClassId('none') ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') - ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) + ->setMsrpDisplayActualPriceType(SourceType::TYPE_IN_CART) ->setPrice(10) ->setWeight(1) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('5.99') - ->save(); + ->setSpecialPrice('5.99'); +$simple1 = $productRepository->save($product); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setId(11) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product2') ->setSku('simple2') ->setTaxClassId('none') ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') - ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_ON_GESTURE) + ->setMsrpDisplayActualPriceType(SourceType::TYPE_ON_GESTURE) ->setPrice(20) ->setWeight(1) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('15.99') - ->save(); + ->setSpecialPrice('15.99'); +$simple2 = $productRepository->save($product); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setId(12) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product3') ->setSku('simple3') ->setTaxClassId('none') @@ -131,44 +141,42 @@ ->setShortDescription('short description') ->setPrice(30) ->setWeight(1) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_DISABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 140, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('25.99') - ->save(); + ->setSpecialPrice('25.99'); +$simple3 = $productRepository->save($product); -$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +/** @var CategoryInterfaceFactory $categoryInterfaceFactory */ +$categoryInterfaceFactory = $objectManager->get(CategoryInterfaceFactory::class); + +$category = $categoryInterfaceFactory->create(); $category->isObjectNew(true); -$category->setId( - 333 -)->setCreatedAt( - '2014-06-23 09:50:07' -)->setName( - 'Category 1' -)->setParentId( - 2 -)->setPath( - '1/2/333' -)->setLevel( - 2 -)->setAvailableSortBy( - ['position', 'name'] -)->setDefaultSortBy( - 'name' -)->setIsActive( - true -)->setPosition( - 1 -)->setPostedProducts( - [10 => 10, 11 => 11, 12 => 12] -)->save(); +$category->setId(333) + ->setCreatedAt('2014-06-23 09:50:07') + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/333') + ->setLevel(2) + ->setAvailableSortBy(['position', 'name']) + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setPostedProducts( + [ + $simple1->getId() => 10, + $simple2->getId() => 11, + $simple3->getId() => 12 + ] + ); +$category->save(); -/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ -$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +/** @var Collection $indexerCollection */ +$indexerCollection = $objectManager->get(Collection::class); $indexerCollection->load(); -/** @var \Magento\Indexer\Model\Indexer $indexer */ +/** @var Indexer $indexer */ foreach ($indexerCollection->getItems() as $indexer) { $indexer->reindexAll(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php index dd89f8974a647..47e6a4e71cb69 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php @@ -5,47 +5,57 @@ */ declare(strict_types=1); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); - +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); foreach (['simple1', 'simple2', 'simple3'] as $sku) { try { $product = $productRepository->get($sku, false, null, true); $productRepository->delete($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + } catch (NoSuchEntityException $exception) { //Product already removed } } -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); -foreach ($productCollection as $product) { - $product->delete(); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$category = $getCategoryByName->execute('Category 1'); +try { + if ($category->getId()) { + $categoryRepository->delete($category); + } +} catch (NoSuchEntityException $exception) { + //Category already removed } -/** @var $category \Magento\Catalog\Model\Category */ -$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); -$category->load(333); -if ($category->getId()) { - $category->delete(); -} +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); -$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); -if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute - && $attribute->getId() -) { - $attribute->delete(); +try { + $attribute = $attributeRepository->get('test_configurable'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $exception) { + //Attribute already removed } $eavConfig->clear(); - $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 4dd088e148d75..76056f2fa9e0d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -5,8 +5,15 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use Magento\TestFramework\Eav\Model\GetAttributeSetByName; @@ -15,136 +22,117 @@ Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/categories.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var GetAttributeSetByName $getAttributeSetByName */ -$getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); -$attributeSet = $getAttributeSetByName->execute('second_attribute_set'); -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); - -$eavConfig->clear(); - -$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); -$eavConfig->clear(); - -/** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); - -if (!$attribute->getId()) { - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute->setData( - [ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default_value' => 'option_0' - ] - ); - $attributeRepository->save($attribute); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); -} -// create a second attribute -if (!$attribute1->getId()) { - - /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute1 = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute1->setData( - [ - 'attribute_code' => 'second_test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Second Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default' => ['option_0'] - ] - ); - - $attributeRepository->save($attribute1); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup( - 'catalog_product', - $attributeSet->getId(), - $attributeSet->getDefaultGroupId(), - $attribute1->getId() - ); -} +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); +/** @var GetAttributeSetByName $getAttributeSetByName */ +$getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); +$secondAttributeSet = $getAttributeSetByName->execute('second_attribute_set'); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$defaultAttributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$defaultGroupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $defaultAttributeSetId); + +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] +); +$attribute = $attributeRepository->save($attributeModel); +$installer->addAttributeToGroup( + Product::ENTITY, + $defaultAttributeSetId, + $defaultGroupId, + $attribute->getId() +); $eavConfig->clear(); -/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var ProductAttributeInterface $attribute */ +$attributeModel2 = $attributeFactory->create(); +$attributeModel2->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'], + ] +); +$attribute2 = $attributeRepository->save($attributeModel2); +$installer->addAttributeToGroup( + Product::ENTITY, + $secondAttributeSet->getId(), + $secondAttributeSet->getDefaultGroupId(), + $attribute2->getId() +); /** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ -$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); $productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; foreach ($productsWithNewAttributeSet as $sku) { try { $product = $productRepository->get($sku, false, null, true); - $product->setAttributeSetId($attributeSet->getId()); + $product->setAttributeSetId($secondAttributeSet->getId()); $product->setStockData( - ['use_config_manage_stock' => 1, + [ + 'use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, - 'is_in_stock' => 1] + 'is_in_stock' => 1, + ] ); $productRepository->save($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php index 12f63993cb2d3..939c1d261b3c6 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php @@ -4,59 +4,67 @@ * See COPYING.txt for license details. */ +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$objectManager = Bootstrap::getObjectManager(); -$eavConfig->clear(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); -/** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); - -if (!$attribute->getId()) { - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute->setData( - [ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 0, - 'is_visible_in_advanced_search' => 0, - 'is_comparable' => 0, - 'is_filterable' => 0, - 'is_filterable_in_search' => 0, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 0, - 'used_in_product_listing' => 0, - 'used_for_sort_by' => 0, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - ] - ); - - $attributeRepository->save($attribute); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +try { + $attributeRepository->get('test_configurable'); + Resolver::getInstance() + ->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php'); +} catch (NoSuchEntityException $e) { } +$eavConfig = $objectManager->get(Config::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] +); + +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); $eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php index f6e6261c75662..618b554aaa2cc 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php @@ -3,49 +3,63 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Setup\CategorySetup; use Magento\ConfigurableProduct\Helper\Product\Options\Factory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Eav\Api\Data\AttributeOptionInterface; -use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager - ->get(ProductRepositoryInterface::class); -/** @var Config $eavConfig */ -$eavConfig = $objectManager->get(Config::class); -$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); -/** @var $installer CategorySetup */ -$installer = $objectManager->create(CategorySetup::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); -/* Create simple products per each option value*/ +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $attribute Attribute */ +$attribute = $attributeRepository->get('test_configurable'); /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); + +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/* Create simple products per each option value*/ + $attributeValues = []; -$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; $productIds = [10, 20]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { /** @var $product Product */ - $product = $objectManager->create(Product::class); + $product = $productInterfaceFactory->create(); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Option' . $option->getLabel()) ->setSku('simple_' . $productId) ->setPrice($productId) @@ -53,20 +67,18 @@ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $product = $productRepository->save($product); + $simple1 = $productRepository->save($product); $attributeValues[] = [ 'label' => 'test', 'attribute_id' => $attribute->getId(), 'value_index' => $option->getValue(), ]; - $associatedProductIds[] = $product->getId(); + $associatedProductIds[] = $simple1->getId(); } /** @var $product Product */ -$product = $objectManager->create(Product::class); -/** @var Factory $optionsFactory */ -$optionsFactory = $objectManager->create(Factory::class); +$product = $productInterfaceFactory->create(); $configurableAttributesData = [ [ 'attribute_id' => $attribute->getId(), @@ -84,7 +96,7 @@ $product->setTypeId(Configurable::TYPE_CODE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Product') ->setSku('configurable') ->setVisibility(Visibility::VISIBILITY_BOTH) @@ -98,18 +110,17 @@ $options = $attribute->getOptions(); $attributeValues = []; -$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; $productIds = [30, 40]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { /** @var $product Product */ - $product = $objectManager->create(Product::class); + $product = $productInterfaceFactory->create(); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Option' . $option->getLabel()) ->setSku('simple_' . $productId) ->setPrice($productId) @@ -117,21 +128,18 @@ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $product = $productRepository->save($product); + $simple2 = $productRepository->save($product); $attributeValues[] = [ 'label' => 'test', 'attribute_id' => $attribute->getId(), 'value_index' => $option->getValue(), ]; - $associatedProductIds[] = $product->getId(); + $associatedProductIds[] = $simple2->getId(); } /** @var $product Product */ -$product = $objectManager->create(Product::class); - -/** @var Factory $optionsFactory */ -$optionsFactory = $objectManager->create(Factory::class); +$product = $productInterfaceFactory->create(); $configurableAttributesData = [ [ @@ -153,7 +161,7 @@ $product->setTypeId(Configurable::TYPE_CODE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Product 12345') ->setSku('configurable_12345') ->setVisibility(Visibility::VISIBILITY_BOTH) diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php new file mode 100644 index 0000000000000..f243d39c24d26 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Indexer\Model\Processor; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Processor $processor */ +$processor = Bootstrap::getObjectManager()->get(Processor::class); +$processor->reindexAllInvalid(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php index 4e64aa0349b80..1b56a7e8c4448 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php @@ -5,4 +5,5 @@ */ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/product_configurable_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php index 38fade9013cd1..0bc5e2e6e595e 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php @@ -12,9 +12,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture( - 'Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php' -); $objectManager = Bootstrap::getObjectManager(); /** @var Registry $registry */ $registry = $objectManager->get(Registry::class); @@ -42,5 +39,9 @@ //Product already removed } +Resolver::getInstance()->requireDataFixture( + 'Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php' +); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php index c480906619a4a..c5e1e1fc287ba 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php @@ -5,20 +5,33 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Swatches\Helper\Media as SwatchesMedia; use Magento\TestFramework\Helper\Bootstrap; +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + +try { + $attribute = $attributeRepository->get('test_configurable'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $exception) { + //Product already removed +} + /** @var WriteInterface $mediaDirectory */ -$mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) +$mediaDirectory = $objectManager->get(Filesystem::class) ->getDirectoryWrite( DirectoryList::MEDIA ); /** @var SwatchesMedia $swatchesMedia */ -$swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); +$swatchesMedia = $objectManager->get(SwatchesMedia::class); $testImageName = 'visual_swatch_attribute_option_type_image.jpg'; $testImageSwatchPath = $swatchesMedia->getAttributeSwatchPath($testImageName); From 4f8f9f2ae93caadd2d63d587183d6ac78a3514c3 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 21 Oct 2020 12:00:21 +0300 Subject: [PATCH 091/195] test coverage --- .../ConfirmCustomerByTokenTest.php | 91 ++++++++++++++++++ .../ConfirmCustomerByTokenTest.php | 92 +++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php diff --git a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php new file mode 100644 index 0000000000000..30aa70e89d2d0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\ForgotPasswordToken; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Model\ResourceModel\Customer; +use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken. + */ +class ConfirmCustomerByTokenTest extends TestCase +{ + private const STUB_RESET_PASSWORD_TOKEN = 'resetPassword'; + + /** + * @var ConfirmCustomerByToken; + */ + private $model; + + /** + * @var CustomerInterface|MockObject + */ + private $customerMock; + + /** + * @var CustomerResource|MockObject + */ + private $customerResourceMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->customerMock = $this->getMockForAbstractClass(CustomerInterface::class); + $this->customerResourceMock = $this->createMock(CustomerResource::class); + + $getCustomerByTokenMock = $this->createMock(GetCustomerByToken::class); + $getCustomerByTokenMock->method('execute')->willReturn($this->customerMock); + + $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerResourceMock); + } + + /** + * Confirm customer with confirmation + * + * @return void + */ + public function testExecuteWithConfirmation(): void + { + $customerId = 777; + + $this->customerMock->expects($this->once()) + ->method('getConfirmation') + ->willReturn('GWz2ik7Kts517MXAgrm4DzfcxKayGCm4'); + $this->customerMock->expects($this->once()) + ->method('getId') + ->willReturn($customerId); + $this->customerResourceMock->expects($this->once()) + ->method('updateColumn') + ->with($customerId, 'confirmation', null); + + $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); + } + + /** + * Confirm customer without confirmation + * + * @return void + */ + public function testExecuteWithoutConfirmation(): void + { + $this->customerMock->expects($this->once()) + ->method('getConfirmation') + ->willReturn(null); + $this->customerResourceMock->expects($this->never()) + ->method('updateColumn'); + + $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php new file mode 100644 index 0000000000000..5399f6903ee9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\ForgotPasswordToken; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken. + */ +class ConfirmCustomerByTokenTest extends TestCase +{ + private const STUB_CUSTOMER_RESET_TOKEN = 'token12345'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ConfirmCustomerByToken + */ + private $confirmCustomerByToken; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + + $resource = $this->objectManager->get(ResourceConnection::class); + $this->connection = $resource->getConnection(); + + $this->confirmCustomerByToken = $this->objectManager->get(ConfirmCustomerByToken::class); + } + + /** + * Customer address shouldn't validate during confirm customer by token + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * + * @return void + */ + public function testExecuteWithInvalidAddress(): void + { + $id = 1; + + $customerModel = $this->objectManager->create(Customer::class); + $customerModel->load($id); + $customerModel->setRpToken(self::STUB_CUSTOMER_RESET_TOKEN); + $customerModel->setRpTokenCreatedAt(date('Y-m-d H:i:s')); + $customerModel->setConfirmation($customerModel->getRandomConfirmationKey()); + $customerModel->save(); + + //make city address invalid + $this->makeCityInvalid($id); + + $this->confirmCustomerByToken->execute(self::STUB_CUSTOMER_RESET_TOKEN); + $this->assertNull($customerModel->load($id)->getConfirmation()); + } + + /** + * Set city invalid for customer address + * + * @param int $id + * @return void + */ + private function makeCityInvalid(int $id): void + { + $this->connection->update( + $this->connection->getTableName('customer_address_entity'), + ['city' => ''], + $this->connection->quoteInto('entity_id = ?', $id) + ); + } +} From 8243e91b186baaa1002cc1cc7f74bbe4f1d219fa Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Wed, 21 Oct 2020 14:50:58 +0300 Subject: [PATCH 092/195] MC-38413: REST API: catalogCategoryLinkManagement error if same product in multiple subcategories of parent --- .../Catalog/Model/CategoryLinkManagement.php | 6 ++++-- .../Catalog/Model/ResourceModel/Category.php | 6 +++--- .../ResourceModel/Product/Collection.php | 12 ++++++----- .../Unit/Model/CategoryLinkManagementTest.php | 5 +++++ .../Api/CategoryLinkManagementTest.php | 21 ++++++++++++++++--- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryLinkManagement.php b/app/code/Magento/Catalog/Model/CategoryLinkManagement.php index 8966848a6d036..591cbc32a0d86 100644 --- a/app/code/Magento/Catalog/Model/CategoryLinkManagement.php +++ b/app/code/Magento/Catalog/Model/CategoryLinkManagement.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model; /** - * Class CategoryLinkManagement + * Represents Category Product Link Management class */ class CategoryLinkManagement implements \Magento\Catalog\Api\CategoryLinkManagementInterface { @@ -56,7 +57,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getAssignedProducts($categoryId) { @@ -65,6 +66,7 @@ public function getAssignedProducts($categoryId) /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $products */ $products = $category->getProductCollection(); $products->addFieldToSelect('position'); + $products->groupByAttribute($products->getProductEntityMetadata()->getIdentifierField()); /** @var \Magento\Catalog\Api\Data\CategoryProductLinkInterface[] $links */ $links = []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 917aafb643b47..e19286efc38c0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -13,7 +13,7 @@ namespace Magento\Catalog\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Catalog\Setup\CategorySetup; use Magento\Framework\App\ObjectManager; @@ -1172,11 +1172,11 @@ public function getCategoryWithChildren(int $categoryId): array return []; } - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField(); $select = $connection->select() ->from( ['cce' => $this->getTable('catalog_category_entity')], - [$linkField, 'parent_id', 'path'] + [$linkField, 'entity_id', 'parent_id', 'path'] )->join( ['cce_int' => $this->getTable('catalog_category_entity_int')], 'cce.' . $linkField . ' = cce_int.' . $linkField, diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 7dbfe0d5fccea..3f908663c8e5e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; @@ -2130,16 +2131,17 @@ private function getChildrenCategories(int $categoryId): array $firstCategory = array_shift($categories); if ($firstCategory['is_anchor'] == 1) { - $linkField = $this->getProductEntityMetadata()->getLinkField(); - $anchorCategory[] = (int)$firstCategory[$linkField]; + //category hierarchy can not be modified by staging updates + $entityField = $this->metadataPool->getMetadata(CategoryInterface::class)->getIdentifierField(); + $anchorCategory[] = (int)$firstCategory[$entityField]; foreach ($categories as $category) { if (in_array($category['parent_id'], $categoryIds) && in_array($category['parent_id'], $anchorCategory)) { - $categoryIds[] = (int)$category[$linkField]; + $categoryIds[] = (int)$category[$entityField]; // Storefront approach is to treat non-anchor children of anchor category as anchors. - // Adding their's IDs to $anchorCategory for consistency. + // Adding theirs IDs to $anchorCategory for consistency. if ($category['is_anchor'] == 1 || in_array($category['parent_id'], $anchorCategory)) { - $anchorCategory[] = (int)$category[$linkField]; + $anchorCategory[] = (int)$category[$entityField]; } } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php index be79b11cdf2b8..7cb2064d34d20 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php @@ -15,6 +15,7 @@ use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Framework\DataObject; use Magento\Framework\Indexer\IndexerRegistry; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -85,7 +86,11 @@ public function testGetAssignedProducts() $categoryMock->expects($this->once())->method('getProductCollection')->willReturn($productsMock); $categoryMock->expects($this->once())->method('getId')->willReturn($categoryId); $productsMock->expects($this->once())->method('addFieldToSelect')->with('position')->willReturnSelf(); + $productsMock->expects($this->once())->method('groupByAttribute')->with('entity_id')->willReturnSelf(); $productsMock->expects($this->once())->method('getItems')->willReturn($items); + $productsMock->expects($this->once()) + ->method('getProductEntityMetadata') + ->willReturn(new DataObject(['identifier_field' => 'entity_id'])); $this->productLinkFactoryMock->expects($this->once())->method('create')->willReturn($categoryProductLinkMock); $categoryProductLinkMock->expects($this->once()) ->method('setSku') diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php index 629cc077a63ea..85509dabdf415 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php @@ -4,10 +4,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Api; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Represents CategoryLinkManagementTest Class + */ class CategoryLinkManagementTest extends WebapiAbstract { const SERVICE_WRITE_NAME = 'catalogCategoryLinkManagementV1'; @@ -43,11 +48,21 @@ public function testInfoNoSuchEntityException() } } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testDuplicatedProductsInChildCategories() + { + $result = $this->getAssignedProducts(3, 'all'); + $this->assertCount(3, $result); + } + /** * @param int $id category id - * @return string + * @param string|null $storeCode + * @return array|string */ - protected function getAssignedProducts($id) + private function getAssignedProducts(int $id, ?string $storeCode = null) { $serviceInfo = [ 'rest' => [ @@ -60,6 +75,6 @@ protected function getAssignedProducts($id) 'operation' => self::SERVICE_WRITE_NAME . 'GetAssignedProducts', ], ]; - return $this->_webApiCall($serviceInfo, ['categoryId' => $id]); + return $this->_webApiCall($serviceInfo, ['categoryId' => $id], null, $storeCode); } } From 8a091cc5ac9f654751fa7495abae33b26ea75e6d Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Thu, 22 Oct 2020 12:18:33 +0300 Subject: [PATCH 093/195] MC-34156: Cannot save product in store view scope without Magento_Catalog::edit_product_design ACL --- .../Initialization/Helper/AttributeFilter.php | 2 +- .../Model/Product/AuthorizationTest.php | 170 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php index 49165c85f85d7..1d6939acacfd0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php @@ -80,7 +80,7 @@ private function prepareDefaultData(array $attributeList, string $attributeCode, // For non-numeric types set the attributeValue to 'false' to trigger their removal from the db if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { $attribute->setIsRequired(false); - $productData[$attributeCode] = false; + $productData[$attributeCode] = $attribute->getDefaultValue() ?: false; } else { $productData[$attributeCode] = null; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php new file mode 100644 index 0000000000000..80de1b3a19270 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Laminas\Stdlib\Parameters; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Verify additional authorization for product operations + */ +class AuthorizationTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Helper + */ + private $initializationHelper; + + /** + * @var HttpRequest + */ + private $request; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheridoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->initializationHelper = $this->objectManager->get(Helper::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->request = $this->objectManager->get(HttpRequest::class); + } + + /** + * Verify AuthorizedSavingOf + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param array $data + * + * @dataProvider postRequestData + */ + public function testAuthorizedSavingOf(array $data): void + { + $this->request->setPost(new Parameters($data)); + + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + + $product = $this->initializationHelper->initialize($product); + $this->assertEquals('simple_new', $product->getName()); + $this->assertEquals( + 'container2', + $product->getCustomAttribute('options_container')->getValue() + ); + } + + /** + * @return array + */ + public function postRequestData(): array + { + return [ + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'custom_design' => '', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_layout_update' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout_update_file' => '', + ], + 'use_default' => [ + 'custom_design' => '1', + 'page_layout' => '1', + 'options_container' => '1', + 'custom_layout' => '1', + 'custom_design_from' => '1', + 'custom_design_to' => '1', + 'custom_layout_update_file' => '1', + ], + ] + ], + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout' => '', + 'custom_layout_update_file' => '__no_update__', + ], + 'use_default' => null, + ] + ], + ]; + } + + /** + * Verify AuthorizedSavingOf when change design attributes + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param array $data + * + * @dataProvider postRequestDataException + * @throws AuthorizationException + */ + public function testAuthorizedSavingOfWithException(array $data): void + { + $this->expectException(AuthorizationException::class); + $this->expectErrorMessage('Not allowed to edit the product\'s design attributes'); + $this->request->setPost(new Parameters($data)); + + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + + $this->initializationHelper->initialize($product); + } + + /** + * @return array + */ + public function postRequestDataException(): array + { + return [ + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'page_layout' => '1column', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout' => '', + 'custom_layout_update_file' => '__no_update__', + ], + 'use_default' => null, + ], + ], + ]; + } +} From e113cd980c0567ded9e1db5cae5e2ac397ffd4dc Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Thu, 22 Oct 2020 15:10:25 +0300 Subject: [PATCH 094/195] MC-38433: CSV import ignores "dropdown" and "textarea" for additional attributes --- .../Import/Product/Type/AbstractType.php | 1 + .../Import/Product/Type/AbstractTypeTest.php | 162 +++++++++++------- 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 6571b16c87565..bd17cfd2cd7f1 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -533,6 +533,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe if ($attrParams['is_static']) { continue; } + $attrCode = mb_strtolower($attrCode); if (isset($rowData[$attrCode]) && strlen(trim($rowData[$attrCode]))) { if (in_array($attrParams['type'], ['select', 'boolean'])) { $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index 08915fb31a8aa..9453075f99e7c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -6,6 +6,7 @@ */ namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType as AbstractType; @@ -13,6 +14,7 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Set; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory as AttributeSetCollectionFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; @@ -68,12 +70,12 @@ protected function setUp(): void { $this->entityModel = $this->createMock(Product::class); $attrSetColFactory = $this->createPartialMock( - \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class, + AttributeSetCollectionFactory::class, ['create'] ); $attrSetCollection = $this->createMock(Collection::class); $attrColFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, + AttributeCollectionFactory::class, ['create'] ); $attributeSet = $this->createMock(Set::class); @@ -100,14 +102,22 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMock(); - $attribute->expects($this->any())->method('getIsVisible')->willReturn(true); - $attribute->expects($this->any())->method('getIsGlobal')->willReturn(true); - $attribute->expects($this->any())->method('getIsRequired')->willReturn(true); - $attribute->expects($this->any())->method('getIsUnique')->willReturn(true); - $attribute->expects($this->any())->method('getFrontendLabel')->willReturn('frontend_label'); - $attribute->expects($this->any())->method('getApplyTo')->willReturn(['simple']); - $attribute->expects($this->any())->method('getDefaultValue')->willReturn('default_value'); - $attribute->expects($this->any())->method('usesSource')->willReturn(true); + $attribute->method('getIsVisible') + ->willReturn(true); + $attribute->method('getIsGlobal') + ->willReturn(true); + $attribute->method('getIsRequired') + ->willReturn(true); + $attribute->method('getIsUnique') + ->willReturn(true); + $attribute->method('getFrontendLabel') + ->willReturn('frontend_label'); + $attribute->method('getApplyTo') + ->willReturn(['simple']); + $attribute->method('getDefaultValue') + ->willReturn('default_value'); + $attribute->method('usesSource') + ->willReturn(true); $entityAttributes = [ [ @@ -123,38 +133,54 @@ protected function setUp(): void $attribute2 = clone $attribute; $attribute3 = clone $attribute; - $attribute1->expects($this->any())->method('getId')->willReturn('1'); - $attribute1->expects($this->any())->method('getAttributeCode')->willReturn('attr_code'); - $attribute1->expects($this->any())->method('getFrontendInput')->willReturn('multiselect'); - $attribute1->expects($this->any())->method('isStatic')->willReturn(true); - - $attribute2->expects($this->any())->method('getId')->willReturn('2'); - $attribute2->expects($this->any())->method('getAttributeCode')->willReturn('boolean_attribute'); - $attribute2->expects($this->any())->method('getFrontendInput')->willReturn('boolean'); - $attribute2->expects($this->any())->method('isStatic')->willReturn(false); - - $attribute3->expects($this->any())->method('getId')->willReturn('3'); - $attribute3->expects($this->any())->method('getAttributeCode')->willReturn('text_attribute'); - $attribute3->expects($this->any())->method('getFrontendInput')->willReturn('text'); - $attribute3->expects($this->any())->method('isStatic')->willReturn(false); - - $this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3); - $this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturnOnConsecutiveCalls( - ['option1', 'option2'], - ['yes' => 1, 'no' => 0] - ); - $attrSetColFactory->expects($this->any())->method('create')->willReturn($attrSetCollection); - $attrSetCollection->expects($this->any())->method('setEntityTypeFilter')->willReturn([$attributeSet]); - $attrColFactory->expects($this->any())->method('create')->willReturn($attrCollection); - $attrCollection->expects($this->any()) - ->method('setAttributeSetFilter') + $attribute1->method('getId') + ->willReturn('1'); + $attribute1->method('getAttributeCode') + ->willReturn('attr_code'); + $attribute1->method('getFrontendInput') + ->willReturn('multiselect'); + $attribute1->method('isStatic') + ->willReturn(true); + + $attribute2->method('getId') + ->willReturn('2'); + $attribute2->method('getAttributeCode') + ->willReturn('boolean_attribute'); + $attribute2->method('getFrontendInput') + ->willReturn('boolean'); + $attribute2->method('isStatic') + ->willReturn(false); + + $attribute3->method('getId') + ->willReturn('3'); + $attribute3->method('getAttributeCode') + ->willReturn('Text_attribute'); + $attribute3->method('getFrontendInput') + ->willReturn('text'); + $attribute3->method('isStatic') + ->willReturn(false); + + $this->entityModel->method('getEntityTypeId') + ->willReturn(3); + $this->entityModel->method('getAttributeOptions') + ->willReturnOnConsecutiveCalls( + ['option1', 'option2'], + ['yes' => 1, 'no' => 0] + ); + $attrSetColFactory->method('create') + ->willReturn($attrSetCollection); + $attrSetCollection->method('setEntityTypeFilter') + ->willReturn([$attributeSet]); + $attrColFactory->method('create') + ->willReturn($attrCollection); + $attrCollection->method('setAttributeSetFilter') ->willReturn([$attribute1, $attribute2, $attribute3]); - $attributeSet->expects($this->any())->method('getId')->willReturn(1); - $attributeSet->expects($this->any())->method('getAttributeSetName')->willReturn('attribute_set_name'); + $attributeSet->method('getId') + ->willReturn(1); + $attributeSet->method('getAttributeSetName') + ->willReturn('attribute_set_name'); - $attrCollection - ->expects($this->any()) - ->method('addFieldToFilter') + $attrCollection->method('addFieldToFilter') ->with( ['main_table.attribute_id', 'main_table.attribute_code'], [ @@ -193,19 +219,26 @@ protected function setUp(): void 'getConnection', ] ); - $this->select->expects($this->any())->method('from')->willReturnSelf(); - $this->select->expects($this->any())->method('where')->willReturnSelf(); - $this->select->expects($this->any())->method('joinLeft')->willReturnSelf(); - $this->connection->expects($this->any())->method('select')->willReturn($this->select); + $this->select->method('from') + ->willReturnSelf(); + $this->select->method('where') + ->willReturnSelf(); + $this->select->method('joinLeft') + ->willReturnSelf(); + $this->connection->method('select') + ->willReturn($this->select); $connection = $this->createMock(Mysql::class); - $connection->expects($this->any())->method('quoteInto')->willReturn('query'); - $this->select->expects($this->any())->method('getConnection')->willReturn($connection); - $this->connection->expects($this->any())->method('insertOnDuplicate')->willReturnSelf(); - $this->connection->expects($this->any())->method('delete')->willReturnSelf(); - $this->connection->expects($this->any())->method('quoteInto')->willReturn(''); - $this->connection - ->expects($this->any()) - ->method('fetchAll') + $connection->method('quoteInto') + ->willReturn('query'); + $this->select->method('getConnection') + ->willReturn($connection); + $this->connection->method('insertOnDuplicate') + ->willReturnSelf(); + $this->connection->method('delete') + ->willReturnSelf(); + $this->connection->method('quoteInto') + ->willReturn(''); + $this->connection->method('fetchAll') ->willReturn($entityAttributes); $this->resource = $this->createPartialMock( @@ -215,12 +248,10 @@ protected function setUp(): void 'getTableName', ] ); - $this->resource->expects($this->any())->method('getConnection')->willReturn( - $this->connection - ); - $this->resource->expects($this->any())->method('getTableName')->willReturn( - 'tableName' - ); + $this->resource->method('getConnection') + ->willReturn($this->connection); + $this->resource->method('getTableName') + ->willReturn('tableName'); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->simpleType = $this->objectManagerHelper->getObject( @@ -233,9 +264,7 @@ protected function setUp(): void ] ); - $this->abstractType = $this->getMockBuilder( - \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class - ) + $this->abstractType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); } @@ -277,8 +306,10 @@ public function testIsRowValidSuccess() { $rowData = ['_attribute_set' => 'attribute_set_name']; $rowNum = 1; - $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(null); - $this->entityModel->expects($this->never())->method('addRowError'); + $this->entityModel->method('getRowScope') + ->willReturn(null); + $this->entityModel->expects($this->never()) + ->method('addRowError'); $this->setPropertyValue( $this->simpleType, '_attributes', @@ -296,8 +327,9 @@ public function testIsRowValidError() 'sku' => 'sku' ]; $rowNum = 1; - $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(1); - $this->entityModel->expects($this->once())->method('addRowError') + $this->entityModel->method('getRowScope') + ->willReturn(1); + $this->entityModel->method('addRowError') ->with( RowValidatorInterface::ERROR_VALUE_IS_REQUIRED, 1, From f43937b0081af90943aeff631447da654e086be4 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Thu, 22 Oct 2020 15:35:25 +0300 Subject: [PATCH 095/195] MC-34156: Cannot save product in store view scope without Magento_Catalog::edit_product_design ACL --- app/code/Magento/Catalog/Model/Product/Authorization.php | 2 +- .../Magento/Catalog/Model/Product/AuthorizationTest.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php index b8aa8f70ba70f..4022eb34e65e3 100644 --- a/app/code/Magento/Catalog/Model/Product/Authorization.php +++ b/app/code/Magento/Catalog/Model/Product/Authorization.php @@ -159,7 +159,7 @@ public function authorizeSavingOf(ProductInterface $product): void if (!$savedProduct->getSku()) { throw NoSuchEntityException::singleField('id', $product->getId()); } - $oldData = $product->getOrigData(); + $oldData = $savedProduct->getData(); } } if ($this->hasProductChanged($product, $oldData)) { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php index 80de1b3a19270..e2b80a975502f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -19,6 +19,8 @@ /** * Verify additional authorization for product operations + * + * @magentoAppArea adminhtml */ class AuthorizationTest extends TestCase { @@ -43,7 +45,7 @@ class AuthorizationTest extends TestCase private $productRepository; /** - * @inheridoc + * @inheritdoc */ protected function setUp(): void { @@ -103,7 +105,7 @@ public function postRequestData(): array 'custom_design_to' => '1', 'custom_layout_update_file' => '1', ], - ] + ], ], [ [ @@ -118,7 +120,7 @@ public function postRequestData(): array 'custom_layout_update_file' => '__no_update__', ], 'use_default' => null, - ] + ], ], ]; } @@ -130,7 +132,6 @@ public function postRequestData(): array * @param array $data * * @dataProvider postRequestDataException - * @throws AuthorizationException */ public function testAuthorizedSavingOfWithException(array $data): void { From 640cad53009b291334234ccd61ab79f256b43da2 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <duhon@users.noreply.github.com> Date: Thu, 22 Oct 2020 16:21:34 -0500 Subject: [PATCH 096/195] [performance] MC-37459: Support by Magento Catalog (#6223) * MC-37459: Support by Magento Catalog --- .htaccess | 400 +-------------- .../Test/Unit/Model/LinkProviderTest.php | 2 +- app/code/Magento/AwsS3/Driver/AwsS3.php | 89 +++- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 30 ++ .../Magento/Backup/Model/Fs/Collection.php | 10 +- .../Captcha/Test/Unit/Helper/DataTest.php | 4 +- .../Captcha/Test/Unit/Model/DefaultTest.php | 6 +- app/code/Magento/Catalog/Helper/Image.php | 27 +- .../Model/Config/CatalogMediaConfig.php | 50 ++ .../Source/Web/CatalogMediaUrlFormat.php | 33 ++ .../Magento/Catalog/Model/Product/Image.php | 21 +- .../Catalog/Model/Product/Media/Config.php | 5 +- .../Magento/Catalog/Model/Template/Filter.php | 2 +- .../Catalog/Model/View/Asset/Image.php | 91 +++- .../Observer/ImageResizeAfterProductSave.php | 21 +- .../Helper/Form/Gallery/ContentTest.php | 454 ------------------ .../Catalog/Test/Unit/Helper/ImageTest.php | 13 +- .../Category/Attribute/Backend/ImageTest.php | 26 +- .../Test/Unit/Model/Category/ImageTest.php | 12 +- .../Test/Unit/Model/ImageUploaderTest.php | 168 ------- .../Model/View/Asset/Image/ContextTest.php | 79 --- .../Test/Unit/Model/View/Asset/ImageTest.php | 213 -------- .../Unit/Model/View/Asset/PlaceholderTest.php | 4 +- .../Product/Listing/Collector/ImageTest.php | 13 +- .../Product/Listing/Collector/Image.php | 16 +- .../Magento/Catalog/etc/adminhtml/system.xml | 7 + app/code/Magento/Catalog/etc/config.xml | 8 +- .../Wysiwyg/Images/Storage/Collection.php | 2 +- .../Test/Unit/Model/Wysiwyg/ConfigTest.php | 8 +- .../Model/Config/Backend/Admin/Robots.php | 8 +- .../Magento/Customer/Model/FileProcessor.php | 3 +- .../Test/Unit/Model/FileProcessorTest.php | 59 +++ .../Unit/Model/Template/Css/ProcessorTest.php | 2 +- .../Test/Unit/Model/UploadImageTest.php | 2 +- .../Unit/Block/Product/View/GalleryTest.php | 2 +- .../Product/Gallery/RetrieveImageTest.php | 15 +- .../RemoteStorage/Plugin/MediaStorage.php | 80 +++ app/code/Magento/RemoteStorage/composer.json | 3 +- app/code/Magento/RemoteStorage/etc/di.xml | 3 + app/code/Magento/RemoteStorage/etc/module.xml | 1 + .../Unit/Model/ItemProvider/ProductTest.php | 2 +- .../Sitemap/Test/Unit/Model/SitemapTest.php | 2 +- .../Test/Unit/Model/_files/sitemap-1-4.xml | 6 +- .../Test/Unit/Model/_files/sitemap-single.xml | 6 +- .../Model/Service/StoreConfigManagerTest.php | 6 +- app/code/Magento/Swatches/Helper/Data.php | 2 +- app/code/Magento/Swatches/Helper/Media.php | 84 +++- .../Swatches/Test/Unit/Helper/MediaTest.php | 42 +- .../Test/Unit/Block/Html/Header/LogoTest.php | 4 +- .../Unit/Model/Design/Backend/FileTest.php | 10 +- .../Config/FileUploader/FileProcessorTest.php | 4 +- .../Test/Unit/Model/Favicon/FaviconTest.php | 2 +- .../dynamic-rows/cells/thumbnail.html | 2 +- .../Test/Unit/Model/Template/FilterTest.php | 4 +- .../Wishlist/CustomerData/Wishlist.php | 27 +- .../Test/Unit/CustomerData/WishlistTest.php | 6 - .../CategoriesQuery/CategoryTreeTest.php | 2 +- .../Magento/GraphQl/Catalog/CategoryTest.php | 2 +- .../Helper/Form/Gallery/ContentTest.php | 4 +- .../Block/Product/View/GalleryTest.php | 102 ++++ .../Adminhtml/Product/Gallery/UploadTest.php | 6 +- .../Catalog/Controller/ProductTest.php | 2 +- .../Catalog/Model/Product/ImageTest.php | 2 +- .../Magento/Cms/Helper/Wysiwyg/ImagesTest.php | 4 +- .../Magento/Cms/Model/Wysiwyg/ConfigTest.php | 2 +- .../Images/GetInsertImageContentTest.php | 2 +- .../Cms/Model/Wysiwyg/Images/StorageTest.php | 11 +- .../Model/Config/Backend/Admin/RobotsTest.php | 13 +- .../Config/Model/_files/no_robots_txt.php | 2 +- .../Config/Model/_files/robots_txt.php | 17 +- .../Magento/Framework/Console/CliTest.php | 130 ----- .../Magento/Framework/Console/_files/env.php | 46 -- .../Framework/Css/_files/css/test-input.html | 2 +- .../_files/_inline_page_expected.html | 14 +- .../testsuite/Magento/Framework/UrlTest.php | 2 +- .../View/Element/AbstractBlockTest.php | 2 +- .../ResourceModel/Catalog/ProductTest.php | 6 +- .../Magento/Store/Model/StoreTest.php | 20 +- .../Widget/Model/Template/FilterTest.php | 4 +- .../Widget/Model/Widget/ConfigTest.php | 2 +- .../TestFramework/Deploy/CliCommand.php | 2 +- index.php | 39 -- .../App/Filesystem/DirectoryList.php | 8 +- .../App/Test/Unit/Config/DocumentRootTest.php | 75 --- .../Magento/Framework/Config/DocumentRoot.php | 6 +- .../Magento/Framework/Console/Cli.php | 23 - .../Framework/Data/Collection/Filesystem.php | 42 +- .../Magento/Framework/File/Uploader.php | 34 +- .../Framework/Filesystem/Directory/Write.php | 2 +- .../Framework/HTTP/PhpEnvironment/Request.php | 3 +- lib/internal/Magento/Framework/Image.php | 4 - .../View/Test/Unit/Design/Theme/ImageTest.php | 14 + nginx.conf.sample | 25 + pub/.htaccess | 102 ++-- pub/index.php | 13 +- .../ImagesGenerator/ImagesGenerator.php | 2 +- .../Model/ConfigOptionsList/Directory.php | 3 +- 97 files changed, 1044 insertions(+), 1953 deletions(-) create mode 100644 app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php create mode 100644 app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php create mode 100644 app/code/Magento/RemoteStorage/Plugin/MediaStorage.php delete mode 100644 dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php delete mode 100644 dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php delete mode 100644 index.php delete mode 100644 lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php diff --git a/.htaccess b/.htaccess index c5f3bf034d2fb..ae929f8bc6467 100644 --- a/.htaccess +++ b/.htaccess @@ -1,393 +1,7 @@ -############################################ -## overrides deployment configuration mode value -## use command bin/magento deploy:mode:set to switch modes - -# SetEnv MAGE_MODE developer - -############################################ -## uncomment these lines for CGI mode -## make sure to specify the correct cgi php binary file name -## it might be /cgi-bin/php-cgi - -# Action php5-cgi /cgi-bin/php5-cgi -# AddHandler php5-cgi .php - -############################################ -## GoDaddy specific options - -# Options -MultiViews - -## you might also need to add this line to php.ini -## cgi.fix_pathinfo = 1 -## if it still doesn't work, rename php.ini to php5.ini - -############################################ -## this line is specific for 1and1 hosting - - #AddType x-mapp-php5 .php - #AddHandler x-mapp-php5 .php - -############################################ -## enable usage of methods arguments in backtrace - - SetEnv MAGE_DEBUG_SHOW_ARGS 1 - -############################################ -## default index file - - DirectoryIndex index.php - -<IfModule mod_php7.c> -############################################ -## adjust memory limit - - php_value memory_limit 756M - php_value max_execution_time 18000 - -############################################ -## disable automatic session start -## before autoload was initialized - - php_flag session.auto_start off - -############################################ -## enable resulting html compression - - #php_flag zlib.output_compression on - -########################################### -## disable user agent verification to not break multiple image upload - - php_flag suhosin.session.cryptua off -</IfModule> -<IfModule mod_security.c> -########################################### -## disable POST processing to not break multiple image upload - - SecFilterEngine Off - SecFilterScanPOST Off -</IfModule> - -<IfModule mod_deflate.c> - -############################################ -## enable apache served files compression -## http://developer.yahoo.com/performance/rules.html#gzip - - # Insert filter on all content - ###SetOutputFilter DEFLATE - # Insert filter on selected content types only - #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/x-javascript application/json image/svg+xml - - # Netscape 4.x has some problems... - #BrowserMatch ^Mozilla/4 gzip-only-text/html - - # Netscape 4.06-4.08 have some more problems - #BrowserMatch ^Mozilla/4\.0[678] no-gzip - - # MSIE masquerades as Netscape, but it is fine - #BrowserMatch \bMSIE !no-gzip !gzip-only-text/html - - # Don't compress images - #SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary - - # Make sure proxies don't deliver the wrong content - #Header append Vary User-Agent env=!dont-vary - -</IfModule> - -<IfModule mod_ssl.c> - -############################################ -## make HTTPS env vars available for CGI mode - - SSLOptions StdEnvVars - -</IfModule> - -############################################ -## workaround for Apache 2.4.6 CentOS build when working via ProxyPassMatch with HHVM (or any other) -## Please, set it on virtual host configuration level - -## SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 -############################################ - -<IfModule mod_rewrite.c> - -############################################ -## enable rewrites - - Options +FollowSymLinks - RewriteEngine on - -############################################ -## you can put here your magento root folder -## path relative to web root - - #RewriteBase /magento/ - -############################################ -## workaround for HTTP authorization -## in CGI environment - - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - -############################################ -## TRACE and TRACK HTTP methods disabled to prevent XSS attacks - - RewriteCond %{REQUEST_METHOD} ^TRAC[EK] - RewriteRule .* - [L,R=405] - -############################################ -## redirect for mobile user agents - - #RewriteCond %{REQUEST_URI} !^/mobiledirectoryhere/.*$ - #RewriteCond %{HTTP_USER_AGENT} "android|blackberry|ipad|iphone|ipod|iemobile|opera mobile|palmos|webos|googlebot-mobile" [NC] - #RewriteRule ^(.*)$ /mobiledirectoryhere/ [L,R=302] - -############################################ -## never rewrite for existing files, directories and links - - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-l - -############################################ -## rewrite everything else to index.php - - RewriteRule .* index.php [L] - -</IfModule> - - -############################################ -## Prevent character encoding issues from server overrides -## If you still have problems, use the second line instead - - AddDefaultCharset Off - #AddDefaultCharset UTF-8 - AddType 'text/html; charset=UTF-8' html - -<IfModule mod_expires.c> - -############################################ -## Add default Expires header -## http://developer.yahoo.com/performance/rules.html#expires - - ExpiresDefault "access plus 1 year" - ExpiresByType text/html A0 - ExpiresByType text/plain A0 - -</IfModule> - -########################################### -## Deny access to root files to hide sensitive application information - RedirectMatch 403 /\.git - - <Files composer.json> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files composer.lock> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .gitignore> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .htaccess> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .htaccess.sample> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .php_cs.dist> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files CHANGELOG.md> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files COPYING.txt> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files Gruntfile.js> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files LICENSE.txt> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files LICENSE_AFL.txt> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files nginx.conf.sample> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files package.json> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files php.ini.sample> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files README.md> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files magento_umask> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files auth.json> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .user.ini> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - -# For 404s and 403s that aren't handled by the application, show plain 404 response -ErrorDocument 404 /pub/errors/404.php -ErrorDocument 403 /pub/errors/404.php - -################################ -## If running in cluster environment, uncomment this -## http://developer.yahoo.com/performance/rules.html#etags - - #FileETag none - -# ###################################################################### -# # INTERNET EXPLORER # -# ###################################################################### - -# ---------------------------------------------------------------------- -# | Document modes | -# ---------------------------------------------------------------------- - -# Force Internet Explorer 8/9/10 to render pages in the highest mode -# available in the various cases when it may not. -# -# https://hsivonen.fi/doctype/#ie8 -# -# (!) Starting with Internet Explorer 11, document modes are deprecated. -# If your business still relies on older web apps and services that were -# designed for older versions of Internet Explorer, you might want to -# consider enabling `Enterprise Mode` throughout your company. -# -# https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode -# http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx - -<IfModule mod_headers.c> - - Header set X-UA-Compatible "IE=edge" - - # `mod_headers` cannot match based on the content-type, however, - # the `X-UA-Compatible` response header should be send only for - # HTML documents and not for the other resources. - - <FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$"> - Header unset X-UA-Compatible - </FilesMatch> - -</IfModule> +RewriteEngine on +RewriteCond %{REQUEST_URI} !^/pub/ +RewriteCond %{REQUEST_URI} !^/setup/ +RewriteCond %{REQUEST_URI} !^/update/ +RewriteCond %{REQUEST_URI} !^/dev/ +RewriteRule .* /pub/$0 [L] +DirectoryIndex index.php diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php index e888c38c4e817..50081d6ae1f17 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php @@ -90,7 +90,7 @@ protected function setUp(): void public function testGet() { - $baseUrl = 'http://magento.local/pub/media/'; + $baseUrl = 'http://magento.local/media/'; $fileInfoPath = 'analytics/data.tgz'; $fileInitializationVector = 'er312esq23eqq'; $this->fileInfoManagerMock->expects($this->once()) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 320e3f9c43a54..b7dee36488bb9 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -11,6 +11,7 @@ use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Phrase; /** * Driver for AWS S3 IO operations. @@ -232,14 +233,14 @@ public function getRealPathSafety($path) */ public function getAbsolutePath($basePath, $path, $scheme = null) { + $basePath = $this->normalizeRelativePath((string)$basePath); + $path = $this->normalizeRelativePath((string)$path); if ($basePath && $path && 0 === strpos($path, $basePath)) { - return $this->normalizeAbsolutePath( - $this->normalizeRelativePath($path) - ); + return $this->normalizeAbsolutePath($path); } if ($basePath && $basePath !== '/') { - return $basePath . ltrim((string)$path, '/'); + $path = $basePath . ltrim((string)$path, '/'); } return $this->normalizeAbsolutePath($path); @@ -328,7 +329,10 @@ public function isDirectory($path): bool $path = rtrim($path, '/') . '/'; - return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_DIR; + if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { + return ($meta['type'] ?? null) === self::TYPE_DIR; + } + return false; } /** @@ -383,7 +387,7 @@ public function stat($path): array $metaInfo = $this->adapter->getMetadata($path); if (!$metaInfo) { - throw new FileSystemException(__('Cannot gather stats! %1', (array)$path)); + throw new FileSystemException(__('Cannot gather stats! %1', [$this->getWarningMessage()])); } return [ @@ -486,7 +490,16 @@ public function touch($path, $modificationTime = null) */ public function fileReadLine($resource, $length, $ending = null): string { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + // phpcs:disable + $result = @stream_get_line($resource, $length, $ending); + // phpcs:enable + if (false === $result) { + throw new FileSystemException( + new Phrase('File cannot be read %1', [$this->getWarningMessage()]) + ); + } + + return $result; } /** @@ -521,7 +534,13 @@ public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure */ public function fileTell($resource): int { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @ftell($resource); + if ($result === null) { + throw new FileSystemException( + new Phrase('An error occurred during "%1" execution.', [$this->getWarningMessage()]) + ); + } + return $result; } /** @@ -529,7 +548,16 @@ public function fileTell($resource): int */ public function fileSeek($resource, $offset, $whence = SEEK_SET): int { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @fseek($resource, $offset, $whence); + if ($result === -1) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileSeek execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -537,7 +565,7 @@ public function fileSeek($resource, $offset, $whence = SEEK_SET): int */ public function endOfFile($resource): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return feof($resource); } /** @@ -554,7 +582,16 @@ public function filePutCsv($resource, array $data, $delimiter = ',', $enclosure */ public function fileFlush($resource): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @fflush($resource); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileFlush execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -562,7 +599,16 @@ public function fileFlush($resource): bool */ public function fileLock($resource, $lockMode = LOCK_EX): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @flock($resource, $lockMode); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileLock execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -570,7 +616,16 @@ public function fileLock($resource, $lockMode = LOCK_EX): bool */ public function fileUnlock($resource): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @flock($resource, LOCK_UN); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileUnlock execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -627,15 +682,11 @@ public function fileOpen($path, $mode) if (!isset($this->streams[$path])) { $this->streams[$path] = tmpfile(); if ($this->adapter->has($path)) { - $file = tmpfile(); //phpcs:ignore Magento2.Functions.DiscouragedFunction - fwrite($file, $this->adapter->read($path)['contents']); + fwrite($this->streams[$path], $this->adapter->read($path)['contents']); //phpcs:ignore Magento2.Functions.DiscouragedFunction - fseek($file, 0); - } else { - $file = tmpfile(); + rewind($this->streams[$path]); } - $this->streams[$path] = $file; } return $this->streams[$path]; diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index b70149e26225c..e3e3e4208484d 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -105,16 +105,46 @@ public function getAbsolutePathDataProvider(): array self::URL . 'test/test.png', self::URL . 'test/test.png' ], + [ + self::URL, + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], + [ + '', + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], [ self::URL . 'test/', 'test.txt', self::URL . 'test/test.txt' ], + [ + self::URL . 'media/', + 'media/image.jpg', + self::URL . 'media/image.jpg' + ], [ self::URL . 'media/', '/catalog/test.png', self::URL . 'media/catalog/test.png' ], + [ + self::URL, + 'var/import/images', + self::URL . 'var/import/images' + ], + [ + self::URL . 'export/', + null, + self::URL . 'export/' + ], + [ + self::URL . 'var/import/images/product_images/', + self::URL . 'var/import/images/product_images/1.png', + self::URL . 'var/import/images/product_images/1.png' + ], [ '', self::URL . 'media/catalog/test.png', diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index 94f555e4054e3..6102a63ec2f69 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -6,8 +6,6 @@ namespace Magento\Backup\Model\Fs; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\DocumentRoot; -use Magento\Framework\Filesystem\Directory\TargetDirectory; /** * Backup data collection @@ -52,20 +50,16 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem * @param \Magento\Backup\Helper\Data $backupData * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Backup\Model\Backup $backup - * @param TargetDirectory|null $targetDirectory - * @param DocumentRoot|null $documentRoot * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, \Magento\Backup\Helper\Data $backupData, \Magento\Framework\Filesystem $filesystem, - \Magento\Backup\Model\Backup $backup, - TargetDirectory $targetDirectory = null, - DocumentRoot $documentRoot = null + \Magento\Backup\Model\Backup $backup ) { $this->_backupData = $backupData; - parent::__construct($entityFactory, $targetDirectory, $documentRoot); + parent::__construct($entityFactory, $filesystem); $this->_filesystem = $filesystem; $this->_backup = $backup; diff --git a/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php b/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php index ec9f6f03134cc..4b9286f69cce5 100644 --- a/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php @@ -197,7 +197,7 @@ public function testGetImgDir() */ public function testGetImgUrl() { - $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/pub/media/captcha/base/'); + $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/media/captcha/base/'); } /** @@ -223,7 +223,7 @@ protected function _getStoreStub() { $store = $this->createMock(Store::class); - $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/pub/media/'); + $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/media/'); return $store; } diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index a20ff898c222e..9e3b0e9a8770f 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -217,7 +217,7 @@ public function testGetImgSrc() { $this->assertEquals( $this->_object->getImgSrc(), - 'http://localhost/pub/media/captcha/base/' . $this->_object->getId() . '.png' + 'http://localhost/media/captcha/base/' . $this->_object->getId() . '.png' ); } @@ -310,7 +310,7 @@ protected function _getHelperStub() )->method( 'getImgUrl' )->willReturn( - 'http://localhost/pub/media/captcha/base/' + 'http://localhost/media/captcha/base/' ); return $helper; @@ -365,7 +365,7 @@ protected function _getStoreStub() ->onlyMethods(['getBaseUrl']) ->disableOriginalConstructor() ->getMock(); - $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/pub/media/'); + $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/media/'); $store->expects($this->any())->method('isAdmin')->willReturn(false); return $store; } diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index ab74b5694ce9f..de32f6b7637d4 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -5,7 +5,10 @@ */ namespace Magento\Catalog\Helper; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\Block\ArgumentInterface; /** @@ -133,27 +136,34 @@ class Image extends AbstractHelper implements ArgumentInterface */ private $viewAssetPlaceholderFactory; + /** + * @var CatalogMediaConfig + */ + private $mediaConfig; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Catalog\Model\Product\ImageFactory $productImageFactory * @param \Magento\Framework\View\Asset\Repository $assetRepo * @param \Magento\Framework\View\ConfigInterface $viewConfig * @param \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory + * @param CatalogMediaConfig $mediaConfig */ public function __construct( \Magento\Framework\App\Helper\Context $context, \Magento\Catalog\Model\Product\ImageFactory $productImageFactory, \Magento\Framework\View\Asset\Repository $assetRepo, \Magento\Framework\View\ConfigInterface $viewConfig, - \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory = null + \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory = null, + CatalogMediaConfig $mediaConfig = null ) { $this->_productImageFactory = $productImageFactory; parent::__construct($context); $this->_assetRepo = $assetRepo; $this->viewConfig = $viewConfig; $this->viewAssetPlaceholderFactory = $placeholderFactory - ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + ?: ObjectManager::getInstance()->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + $this->mediaConfig = $mediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); } /** @@ -532,7 +542,16 @@ protected function isScheduledActionsAllowed() public function getUrl() { try { - $this->applyScheduledActions(); + switch ($this->mediaConfig->getMediaUrlFormat()) { + case CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS: + $this->initBaseFile(); + break; + case CatalogMediaConfig::HASH: + $this->applyScheduledActions(); + break; + default: + throw new LocalizedException(__("The specified Catalog media URL format is not supported.")); + } return $this->_getModel()->getUrl(); } catch (\Exception $e) { return $this->getDefaultPlaceholderUrl(); diff --git a/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php new file mode 100644 index 0000000000000..0ae128b34d348 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Config; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Config for catalog media + */ +class CatalogMediaConfig +{ + private const XML_PATH_CATALOG_MEDIA_URL_FORMAT = 'web/url/catalog_media_url_format'; + + const IMAGE_OPTIMIZATION_PARAMETERS = 'image_optimization_parameters'; + const HASH = 'hash'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get media URL format for catalog images + * + * @param string $scopeType + * @param null|int|string $scopeCode + * @return string + */ + public function getMediaUrlFormat($scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null): string + { + return $this->scopeConfig->getValue( + CatalogMediaConfig::XML_PATH_CATALOG_MEDIA_URL_FORMAT, + $scopeType, + $scopeCode + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php b/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php new file mode 100644 index 0000000000000..bab2d5ccb3f1f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Config\Source\Web; + +use Magento\Catalog\Model\Config\CatalogMediaConfig; + +/** + * Option provider for catalog media URL format system setting. + */ +class CatalogMediaUrlFormat implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * Get a list of supported catalog media URL formats. + * + * @codeCoverageIgnore + * @return array + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS, + 'label' => __('Image optimization based on query parameters') + ], + ['value' => CatalogMediaConfig::HASH, 'label' => __('Unique hash per image variant (Legacy mode)')] + ]; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index 3c60d81e9a4d8..842ee197f83fe 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -10,9 +10,11 @@ use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Image as MagentoImage; use Magento\Framework\Serialize\SerializerInterface; use Magento\Catalog\Model\Product\Image\ParamsBuilder; +use Magento\Framework\Filesystem\Driver\File as FilesystemDriver; /** * Image operations @@ -101,6 +103,7 @@ class Image extends \Magento\Framework\Model\AbstractModel /** * @var int + * @deprecated unused */ protected $_angle; @@ -199,6 +202,11 @@ class Image extends \Magento\Framework\Model\AbstractModel */ private $serializer; + /** + * @var FilesystemDriver + */ + private $filesystemDriver; + /** * Constructor * @@ -219,6 +227,8 @@ class Image extends \Magento\Framework\Model\AbstractModel * @param array $data * @param SerializerInterface $serializer * @param ParamsBuilder $paramsBuilder + * @param FilesystemDriver $filesystemDriver + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -239,7 +249,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], SerializerInterface $serializer = null, - ParamsBuilder $paramsBuilder = null + ParamsBuilder $paramsBuilder = null, + FilesystemDriver $filesystemDriver = null ) { $this->_storeManager = $storeManager; $this->_catalogProductMediaConfig = $catalogProductMediaConfig; @@ -254,6 +265,7 @@ public function __construct( $this->viewAssetPlaceholderFactory = $viewAssetPlaceholderFactory; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->paramsBuilder = $paramsBuilder ?: ObjectManager::getInstance()->get(ParamsBuilder::class); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance()->get(FilesystemDriver::class); } /** @@ -663,7 +675,12 @@ public function getDestinationSubdir() public function isCached() { $path = $this->imageAsset->getPath(); - return is_array($this->loadImageInfoFromCache($path)) || file_exists($path); + try { + $isCached = is_array($this->loadImageInfoFromCache($path)) || $this->filesystemDriver->isExists($path); + } catch (FileSystemException $e) { + $isCached = false; + } + return $isCached; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 33af93db13b4c..71e29515791a7 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product\Media; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -76,7 +77,7 @@ public function getBaseMediaPath() public function getBaseMediaUrl() { return $this->storeManager->getStore() - ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . 'catalog/product'; + ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $this->getBaseMediaUrlAddition(); } /** @@ -97,7 +98,7 @@ public function getBaseTmpMediaPath() public function getBaseTmpMediaUrl() { return $this->storeManager->getStore()->getBaseUrl( - \Magento\Framework\UrlInterface::URL_TYPE_MEDIA + UrlInterface::URL_TYPE_MEDIA ) . 'tmp/' . $this->getBaseMediaUrlAddition(); } diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 0a46af3ef021d..bf624c3435103 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -108,7 +108,7 @@ public function viewDirective($construction) * The original intent of _absolute parameter was to simply append specified path to a base URL * bypassing any kind of processing. * For example, normally you would use {{view url="css/styles.css"}} directive which would automatically resolve - * into something like http://example.com/pub/static/area/theme/en_US/css/styles.css + * into something like http://example.com/static/area/theme/en_US/css/styles.css * But with _absolute, the expected behavior is this: {{view url="favicon.ico" _absolute=true}} should resolve * into something like http://example.com/favicon.ico * diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php index c547ec612bb94..0f7082f9df154 100644 --- a/app/code/Magento/Catalog/Model/View/Asset/Image.php +++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php @@ -6,11 +6,16 @@ namespace Magento\Catalog\Model\View\Asset; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Asset\ContextInterface; use Magento\Framework\View\Asset\LocalInterface; +use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; /** * A locally available image file asset that can be referred with a file path @@ -58,6 +63,21 @@ class Image implements LocalInterface */ private $encryptor; + /** + * @var ImageHelper + */ + private $imageHelper; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var string + */ + private $mediaFormatUrl; + /** * Image constructor. * @@ -66,13 +86,19 @@ class Image implements LocalInterface * @param EncryptorInterface $encryptor * @param string $filePath * @param array $miscParams + * @param ImageHelper $imageHelper + * @param CatalogMediaConfig $catalogMediaConfig + * @param StoreManagerInterface $storeManager */ public function __construct( ConfigInterface $mediaConfig, ContextInterface $context, EncryptorInterface $encryptor, $filePath, - array $miscParams + array $miscParams, + ImageHelper $imageHelper = null, + CatalogMediaConfig $catalogMediaConfig = null, + StoreManagerInterface $storeManager = null ) { if (isset($miscParams['image_type'])) { $this->sourceContentType = $miscParams['image_type']; @@ -85,14 +111,73 @@ public function __construct( $this->filePath = $filePath; $this->miscParams = $miscParams; $this->encryptor = $encryptor; + $this->imageHelper = $imageHelper ?: ObjectManager::getInstance()->get(ImageHelper::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + + $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaFormatUrl = $catalogMediaConfig->getMediaUrlFormat(); } /** - * @inheritdoc + * Get catalog image URL. + * + * @return string + * @throws LocalizedException */ public function getUrl() { - return $this->context->getBaseUrl() . DIRECTORY_SEPARATOR . $this->getImageInfo(); + switch ($this->mediaFormatUrl) { + case CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS: + return $this->getUrlWithTransformationParameters(); + case CatalogMediaConfig::HASH: + return $this->context->getBaseUrl() . DIRECTORY_SEPARATOR . $this->getImageInfo(); + default: + throw new LocalizedException( + __("The specified Catalog media URL format '$this->mediaFormatUrl' is not supported.") + ); + } + } + + /** + * Get image URL with transformation parameters + * + * @return string + */ + private function getUrlWithTransformationParameters() + { + return $this->getOriginalImageUrl() . '?' . http_build_query($this->getImageTransformationParameters()); + } + + /** + * The list of parameters to be used during image transformations (e.g. resizing or applying watermarks). + * + * This method can be used as an extension point. + * + * @return string[] + */ + public function getImageTransformationParameters() + { + return [ + 'width' => $this->miscParams['image_width'], + 'height' => $this->miscParams['image_height'], + 'store' => $this->storeManager->getStore()->getCode(), + 'image-type' => $this->sourceContentType + ]; + } + + /** + * Get URL to the original version of the product image. + * + * @return string + */ + private function getOriginalImageUrl() + { + $originalImageFile = $this->getSourceFile(); + if (!$originalImageFile) { + return $this->imageHelper->getDefaultPlaceholderUrl(); + } else { + return $this->context->getBaseUrl() . $this->getFilePath(); + } } /** diff --git a/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php b/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php index 91d2868afab8c..54b655a217a08 100644 --- a/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php +++ b/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php @@ -10,7 +10,11 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Framework\App\State; use Magento\MediaStorage\Service\ImageResize; +use Magento\Catalog\Model\Config\CatalogMediaConfig; +/** + * Resize product images after the product is saved + */ class ImageResizeAfterProductSave implements ObserverInterface { /** @@ -23,17 +27,26 @@ class ImageResizeAfterProductSave implements ObserverInterface */ private $state; + /** + * @var CatalogMediaConfig + */ + private $catalogMediaConfig; + /** * Product constructor. + * * @param ImageResize $imageResize * @param State $state + * @param CatalogMediaConfig $catalogMediaConfig */ public function __construct( ImageResize $imageResize, - State $state + State $state, + CatalogMediaConfig $catalogMediaConfig ) { $this->imageResize = $imageResize; $this->state = $state; + $this->catalogMediaConfig = $catalogMediaConfig; } /** @@ -44,6 +57,12 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + $catalogMediaUrlFormat = $this->catalogMediaConfig->getMediaUrlFormat(); + if ($catalogMediaUrlFormat == CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS) { + // Skip image resizing on the Magento side when it is offloaded to a web server or CDN + return; + } + /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php deleted file mode 100644 index 572dbc4ca2732..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ /dev/null @@ -1,454 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery; - -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content; -use Magento\Catalog\Helper\Image; -use Magento\Catalog\Model\Entity\Attribute; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Media\Config; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\Read; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Json\EncoderInterface; -use Magento\Framework\Phrase; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\MediaStorage\Helper\File\Storage\Database; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ContentTest extends TestCase -{ - /** - * @var Filesystem|MockObject - */ - protected $fileSystemMock; - - /** - * @var Read|MockObject - */ - protected $readMock; - - /** - * @var Content|MockObject - */ - protected $content; - - /** - * @var Config|MockObject - */ - protected $mediaConfigMock; - - /** - * @var EncoderInterface|MockObject - */ - protected $jsonEncoderMock; - - /** - * @var Gallery|MockObject - */ - protected $galleryMock; - - /** - * @var Image|MockObject - */ - protected $imageHelper; - - /** - * @var Database|MockObject - */ - protected $databaseMock; - - /** - * @var ObjectManager - */ - protected $objectManager; - - protected function setUp(): void - { - $this->fileSystemMock = $this->getMockBuilder(Filesystem::class) - ->addMethods(['stat']) - ->onlyMethods(['getDirectoryRead']) - ->disableOriginalConstructor() - ->getMock(); - $this->readMock = $this->getMockForAbstractClass(ReadInterface::class); - $this->galleryMock = $this->createMock(Gallery::class); - $this->mediaConfigMock = $this->createPartialMock( - Config::class, - ['getMediaUrl', 'getMediaPath'] - ); - $this->jsonEncoderMock = $this->getMockBuilder(EncoderInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->databaseMock = $this->getMockBuilder(Database::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->objectManager = new ObjectManager($this); - $this->content = $this->objectManager->getObject( - Content::class, - [ - 'mediaConfig' => $this->mediaConfigMock, - 'jsonEncoder' => $this->jsonEncoderMock, - 'filesystem' => $this->fileSystemMock, - 'fileStorageDatabase' => $this->databaseMock - ] - ); - } - - public function testGetImagesJson() - { - $url = [ - ['file_1.jpg', 'url_to_the_image/image_1.jpg'], - ['file_2.jpg', 'url_to_the_image/image_2.jpg'] - ]; - $mediaPath = [ - ['file_1.jpg', 'catalog/product/image_1.jpg'], - ['file_2.jpg', 'catalog/product/image_2.jpg'] - ]; - - $sizeMap = [ - ['catalog/product/image_1.jpg', ['size' => 399659]], - ['catalog/product/image_2.jpg', ['size' => 879394]] - ]; - - $imagesResult = [ - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0', - 'url' => 'url_to_the_image/image_2.jpg', - 'size' => 879394 - ], - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1', - 'url' => 'url_to_the_image/image_1.jpg', - 'size' => 399659 - ] - ]; - - $images = [ - 'images' => [ - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1' - ] , - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn($images); - $this->fileSystemMock->expects($this->once())->method('getDirectoryRead')->willReturn($this->readMock); - - $this->mediaConfigMock->method('getMediaUrl')->willReturnMap($url); - $this->mediaConfigMock->method('getMediaPath')->willReturnMap($mediaPath); - $this->readMock->method('stat')->willReturnMap($sizeMap); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - - $this->readMock->method('isFile')->willReturn(true); - $this->databaseMock->method('checkDbUsage')->willReturn(false); - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - - public function testGetImagesJsonWithoutImages() - { - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn(null); - - $this->assertSame('[]', $this->content->getImagesJson()); - } - - public function testGetImagesJsonWithException() - { - $this->imageHelper = $this->getMockBuilder(Image::class) - ->disableOriginalConstructor() - ->setMethods(['getDefaultPlaceholderUrl']) - ->getMock(); - - $this->objectManager->setBackwardCompatibleProperty( - $this->content, - 'imageHelper', - $this->imageHelper - ); - - $placeholderUrl = 'url_to_the_placeholder/placeholder.jpg'; - - $imagesResult = [ - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0', - 'url' => 'url_to_the_placeholder/placeholder.jpg', - 'size' => 0 - ], - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1', - 'url' => 'url_to_the_placeholder/placeholder.jpg', - 'size' => 0 - ] - ]; - - $images = [ - 'images' => [ - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1' - ], - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn($images); - $this->fileSystemMock->method('getDirectoryRead')->willReturn($this->readMock); - $this->mediaConfigMock->method('getMediaUrl'); - $this->mediaConfigMock->method('getMediaPath'); - - $this->readMock - ->method('isFile') - ->willReturn(true); - $this->databaseMock - ->method('checkDbUsage') - ->willReturn(false); - - $this->readMock->method('stat')->willReturnOnConsecutiveCalls( - $this->throwException( - new FileSystemException(new Phrase('test')) - ), - $this->throwException( - new FileSystemException(new Phrase('test')) - ) - ); - $this->imageHelper->method('getDefaultPlaceholderUrl')->willReturn($placeholderUrl); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - - /** - * Test GetImageTypes() will return value for given attribute from data persistor. - * - * @return void - */ - public function testGetImageTypesFromDataPersistor() - { - $attributeCode = 'thumbnail'; - $value = 'testImageValue'; - $scopeLabel = 'testScopeLabel'; - $label = 'testLabel'; - $name = 'testName'; - $expectedTypes = [ - $attributeCode => [ - 'code' => $attributeCode, - 'value' => $value, - 'label' => $label, - 'name' => $name, - ], - ]; - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $product->expects($this->once()) - ->method('getData') - ->with($this->identicalTo($attributeCode)) - ->willReturn(null); - $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); - $product->expects($this->once()) - ->method('getMediaAttributes') - ->willReturn([$mediaAttribute]); - $this->galleryMock->expects($this->exactly(2)) - ->method('getDataObject') - ->willReturn($product); - $this->galleryMock->expects($this->once()) - ->method('getImageValue') - ->with($this->identicalTo($attributeCode)) - ->willReturn($value); - $this->galleryMock->expects($this->once()) - ->method('getScopeLabel') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($scopeLabel); - $this->galleryMock->expects($this->once()) - ->method('getAttributeFieldName') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($name); - $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); - } - - /** - * Test GetImageTypes() will return value for given attribute from product. - * - * @return void - */ - public function testGetImageTypesFromProduct() - { - $attributeCode = 'thumbnail'; - $value = 'testImageValue'; - $scopeLabel = 'testScopeLabel'; - $label = 'testLabel'; - $name = 'testName'; - $expectedTypes = [ - $attributeCode => [ - 'code' => $attributeCode, - 'value' => $value, - 'label' => $label, - 'name' => $name, - ], - ]; - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $product->expects($this->once()) - ->method('getData') - ->with($this->identicalTo($attributeCode)) - ->willReturn($value); - $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); - $product->expects($this->once()) - ->method('getMediaAttributes') - ->willReturn([$mediaAttribute]); - $this->galleryMock->expects($this->exactly(2)) - ->method('getDataObject') - ->willReturn($product); - $this->galleryMock->expects($this->never()) - ->method('getImageValue'); - $this->galleryMock->expects($this->once()) - ->method('getScopeLabel') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($scopeLabel); - $this->galleryMock->expects($this->once()) - ->method('getAttributeFieldName') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($name); - $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); - } - - /** - * Perform assertions. - * - * @param string $attributeCode - * @param string $scopeLabel - * @param array $expectedTypes - * @return void - */ - private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes) - { - $this->content->setElement($this->galleryMock); - $result = $this->content->getImageTypes(); - $scope = $result[$attributeCode]['scope']; - $this->assertSame($scopeLabel, $scope->getText()); - unset($result[$attributeCode]['scope']); - $this->assertSame($expectedTypes, $result); - } - - /** - * Get media attribute mock. - * - * @param string $label - * @param string $attributeCode - * @return MockObject - */ - private function getMediaAttribute(string $label, string $attributeCode) - { - $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class) - ->disableOriginalConstructor() - ->getMock(); - $frontend->expects($this->once()) - ->method('getLabel') - ->willReturn($label); - $mediaAttribute = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - $mediaAttribute - ->method('getAttributeCode') - ->willReturn($attributeCode); - $mediaAttribute->expects($this->once()) - ->method('getFrontend') - ->willReturn($frontend); - - return $mediaAttribute; - } - - /** - * Test GetImagesJson() calls MediaStorage functions to obtain image from DB prior to stat call - * - * @return void - */ - public function testGetImagesJsonMediaStorageMode() - { - $images = [ - 'images' => [ - [ - 'value_id' => '0', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $mediaPath = [ - ['file_1.jpg', 'catalog/product/image_1.jpg'] - ]; - - $this->content->setElement($this->galleryMock); - - $this->galleryMock->expects($this->once()) - ->method('getImages') - ->willReturn($images); - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->readMock); - $this->mediaConfigMock - ->method('getMediaPath') - ->willReturnMap($mediaPath); - - $this->readMock - ->method('isFile') - ->willReturn(false); - $this->databaseMock - ->method('checkDbUsage') - ->willReturn(true); - - $this->databaseMock->expects($this->once()) - ->method('saveFileToFilesystem') - ->with('catalog/product/image_1.jpg'); - - $this->readMock->method('stat')->willReturn(['size' => 123]); - - $this->content->getImagesJson(); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index c606b7537cc44..125fd287cd4ce 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Test\Unit\Helper; use Magento\Catalog\Helper\Image; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\ImageFactory as ProductImageFactory; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; @@ -70,6 +71,11 @@ class ImageTest extends TestCase */ protected $placeholderFactory; + /** + * @var CatalogMediaConfig|MockObject + */ + private $catalogMediaConfigMock; + protected function setUp(): void { $this->mockContext(); @@ -90,12 +96,17 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $this->catalogMediaConfigMock = $this->createPartialMock(CatalogMediaConfig::class, ['getMediaUrlFormat']); + $this->catalogMediaConfigMock->method('getMediaUrlFormat')->willReturn(CatalogMediaConfig::HASH); + + $this->helper = new Image( $this->context, $this->imageFactory, $this->assetRepository, $this->viewConfig, - $this->placeholderFactory + $this->placeholderFactory, + $this->catalogMediaConfigMock ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php index 16771214026f0..23136e55a2307 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -14,7 +14,9 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -186,6 +188,7 @@ public function testBeforeSaveValueInvalid($value) */ public function testBeforeSaveAttributeFileName() { + $this->setupObjectManagerForCheckImageExist(false); $this->attribute->expects($this->once()) ->method('getName') ->willReturn('test_attribute'); @@ -253,11 +256,23 @@ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() ); } + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + /** * Test beforeSaveTemporaryAttribute. */ public function testBeforeSaveTemporaryAttribute() { + $this->setupObjectManagerForCheckImageExist(false); $this->attribute->expects($this->once()) ->method('getName') ->willReturn('test_attribute'); @@ -268,7 +283,7 @@ public function testBeforeSaveTemporaryAttribute() $this->storeMock->expects($this->once()) ->method('getBaseMediaDir') - ->willReturn('pub/media'); + ->willReturn('media'); $model = $this->setUpModelForTests(); $model->setAttribute($this->attribute); @@ -279,7 +294,9 @@ public function testBeforeSaveTemporaryAttribute() ->with(DirectoryList::MEDIA) ->willReturn($mediaDirectoryMock); - $this->imageUploader->expects($this->any())->method('moveFileFromTmp')->willReturn('test123.jpg'); + $mediaDirectoryMock->method('getAbsolutePath')->willReturn('/media/test123.jpg'); + + $this->imageUploader->method('moveFileFromTmp')->willReturn('test123.jpg'); $object = new DataObject( [ @@ -287,7 +304,7 @@ public function testBeforeSaveTemporaryAttribute() [ 'name' => 'test123.jpg', 'tmp_name' => 'abc123', - 'url' => 'http://www.example.com/pub/media/temp/test123.jpg' + 'url' => 'http://www.example.com/media/temp/test123.jpg' ], ], ] @@ -297,7 +314,7 @@ public function testBeforeSaveTemporaryAttribute() $this->assertEquals( [ - ['name' => '/pub/media/test123.jpg', 'tmp_name' => 'abc123', 'url' => '/pub/media/test123.jpg'], + ['name' => '/media/test123.jpg', 'tmp_name' => 'abc123', 'url' => '/media/test123.jpg'], ], $object->getData('_additional_data_test_attribute') ); @@ -418,6 +435,7 @@ public function testBeforeSaveWithoutAdditionalData($value) */ public function testBeforeSaveWithExceptions() { + $this->setupObjectManagerForCheckImageExist(false); $model = $this->setUpModelForTests(); $this->storeManagerInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php index 42a3031ae27e0..676cf07912f1d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php @@ -84,8 +84,8 @@ public function getUrlDataProvider() ], [ 'testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/catalog/category/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/catalog/category/testimage' ], [ 'testimage', @@ -94,8 +94,8 @@ public function getUrlDataProvider() ], [ '/pub/media/catalog/category/testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/catalog/category/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/catalog/category/testimage' ], [ '/pub/media/catalog/category/testimage', @@ -104,8 +104,8 @@ public function getUrlDataProvider() ], [ '/pub/media/posters/testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/posters/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/posters/testimage' ], [ '/pub/media/posters/testimage', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php deleted file mode 100644 index 93bb85abced75..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php +++ /dev/null @@ -1,168 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model; - -use Magento\Catalog\Model\ImageUploader; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\MediaStorage\Helper\File\Storage\Database; -use Magento\MediaStorage\Model\File\Uploader; -use Magento\MediaStorage\Model\File\UploaderFactory; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -class ImageUploaderTest extends TestCase -{ - /** - * @var ImageUploader - */ - private $imageUploader; - - /** - * Core file storage database - * - * @var Database|MockObject - */ - private $coreFileStorageDatabaseMock; - - /** - * Media directory object (writable). - * - * @var Filesystem|MockObject - */ - private $mediaDirectoryMock; - - /** - * Media directory object (writable). - * - * @var WriteInterface|MockObject - */ - private $mediaWriteDirectoryMock; - - /** - * Uploader factory - * - * @var UploaderFactory|MockObject - */ - private $uploaderFactoryMock; - - /** - * Store manager - * - * @var StoreManagerInterface|MockObject - */ - private $storeManagerMock; - - /** - * @var LoggerInterface|MockObject - */ - private $loggerMock; - - /** - * Base tmp path - * - * @var string - */ - private $baseTmpPath; - - /** - * Base path - * - * @var string - */ - private $basePath; - - /** - * Allowed extensions - * - * @var array - */ - private $allowedExtensions; - - /** - * Allowed mime types - * - * @var array - */ - private $allowedMimeTypes; - - protected function setUp(): void - { - $this->coreFileStorageDatabaseMock = $this->createMock( - Database::class - ); - $this->mediaDirectoryMock = $this->createMock( - Filesystem::class - ); - $this->mediaWriteDirectoryMock = $this->createMock( - WriteInterface::class - ); - $this->mediaDirectoryMock->expects($this->any())->method('getDirectoryWrite')->willReturn( - $this->mediaWriteDirectoryMock - ); - $this->uploaderFactoryMock = $this->createMock( - UploaderFactory::class - ); - $this->storeManagerMock = $this->createMock( - StoreManagerInterface::class - ); - $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $this->baseTmpPath = 'base/tmp/'; - $this->basePath = 'base/real/'; - $this->allowedExtensions = ['.jpg']; - $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; - - $this->imageUploader = - new ImageUploader( - $this->coreFileStorageDatabaseMock, - $this->mediaDirectoryMock, - $this->uploaderFactoryMock, - $this->storeManagerMock, - $this->loggerMock, - $this->baseTmpPath, - $this->basePath, - $this->allowedExtensions, - $this->allowedMimeTypes - ); - } - - public function testSaveFileToTmpDir() - { - $fileId = 'file.jpg'; - $allowedMimeTypes = [ - 'image/jpg', - 'image/jpeg', - 'image/gif', - 'image/png', - ]; - /** @var \Magento\MediaStorage\Model\File\Uploader|MockObject $uploader */ - $uploader = $this->createMock(Uploader::class); - $this->uploaderFactoryMock->expects($this->once())->method('create')->willReturn($uploader); - $uploader->expects($this->once())->method('setAllowedExtensions')->with($this->allowedExtensions); - $uploader->expects($this->once())->method('setAllowRenameFiles')->with(true); - $this->mediaWriteDirectoryMock->expects($this->once())->method('getAbsolutePath')->with($this->baseTmpPath) - ->willReturn($this->basePath); - $uploader->expects($this->once())->method('save')->with($this->basePath) - ->willReturn(['tmp_name' => $this->baseTmpPath, 'file' => $fileId, 'path' => $this->basePath]); - $uploader->expects($this->atLeastOnce())->method('checkMimeType')->with($allowedMimeTypes)->willReturn(true); - $storeMock = $this->createPartialMock( - Store::class, - ['getBaseUrl'] - ); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getBaseUrl'); - $this->coreFileStorageDatabaseMock->expects($this->once())->method('saveFile'); - - $result = $this->imageUploader->saveFileToTmpDir($fileId); - - $this->assertArrayNotHasKey('path', $result); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php deleted file mode 100644 index af8245de3525d..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\View\Asset\Image; - -use Magento\Catalog\Model\Product\Media\ConfigInterface; -use Magento\Catalog\Model\View\Asset\Image\Context; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ContextTest extends TestCase -{ - /** - * @var Context - */ - protected $model; - - /** - * @var WriteInterface|MockObject - */ - protected $mediaDirectory; - - /** - * @var ContextInterface|MockObject - */ - protected $mediaConfig; - - /** - * @var Filesystem|MockObject - */ - protected $filesystem; - - protected function setUp(): void - { - $this->mediaConfig = $this->getMockBuilder(ConfigInterface::class) - ->getMockForAbstractClass(); - $this->mediaConfig->expects($this->any())->method('getBaseMediaPath')->willReturn('catalog/product'); - $this->mediaDirectory = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - $this->mediaDirectory->expects($this->once())->method('create')->with('catalog/product'); - $this->filesystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystem->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->willReturn($this->mediaDirectory); - $this->model = new Context( - $this->mediaConfig, - $this->filesystem - ); - } - - public function testGetPath() - { - $path = '/var/www/html/magento2ce/pub/media/catalog/product'; - $this->mediaDirectory->expects($this->once()) - ->method('getAbsolutePath') - ->with('catalog/product') - ->willReturn($path); - - $this->assertEquals($path, $this->model->getPath()); - } - - public function testGetUrl() - { - $baseUrl = 'http://localhost/pub/media/catalog/product'; - $this->mediaConfig->expects($this->once())->method('getBaseMediaUrl')->willReturn($baseUrl); - - $this->assertEquals($baseUrl, $this->model->getBaseUrl()); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php deleted file mode 100644 index 1a61cd4d4eea8..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php +++ /dev/null @@ -1,213 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\View\Asset; - -use Magento\Catalog\Model\Product\Media\ConfigInterface; -use Magento\Catalog\Model\View\Asset\Image; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\View\Asset\ContextInterface; -use Magento\Framework\View\Asset\Repository; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ImageTest extends TestCase -{ - /** - * @var Image - */ - protected $model; - - /** - * @var ContextInterface|MockObject - */ - protected $mediaConfig; - - /** - * @var EncryptorInterface|MockObject - */ - protected $encryptor; - - /** - * @var ContextInterface|MockObject - */ - protected $context; - - /** - * @var Repository|MockObject - */ - private $assetRepo; - - private $objectManager; - - protected function setUp(): void - { - $this->mediaConfig = $this->getMockForAbstractClass(ConfigInterface::class); - $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); - $this->context = $this->getMockForAbstractClass(ContextInterface::class); - $this->assetRepo = $this->createMock(Repository::class); - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'imageContext' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => '/somefile.png', - 'assetRepo' => $this->assetRepo, - 'miscParams' => [ - 'image_width' => 100, - 'image_height' => 50, - 'constrain_only' => false, - 'keep_aspect_ratio' => false, - 'keep_frame' => true, - 'keep_transparency' => false, - 'background' => '255,255,255', - 'image_type' => 'image', //thumbnail,small_image,image,swatch_image,swatch_thumb - 'quality' => 80, - 'angle' => null - ] - ] - ); - } - - public function testModuleAndContentAndContentType() - { - $contentType = 'image'; - $this->assertEquals($contentType, $this->model->getContentType()); - $this->assertEquals($contentType, $this->model->getSourceContentType()); - $this->assertNull($this->model->getContent()); - $this->assertEquals('cache', $this->model->getModule()); - } - - public function testGetFilePath() - { - $this->assertEquals('/somefile.png', $this->model->getFilePath()); - } - - public function testGetSoureFile() - { - $this->mediaConfig->expects($this->once())->method('getBaseMediaPath')->willReturn('catalog/product'); - $this->assertEquals('catalog/product/somefile.png', $this->model->getSourceFile()); - } - - public function testGetContext() - { - $this->assertInstanceOf(ContextInterface::class, $this->model->getContext()); - } - - /** - * @param string $filePath - * @param array $miscParams - * @param string $readableParams - * @dataProvider getPathDataProvider - */ - public function testGetPath($filePath, $miscParams, $readableParams) - { - $imageModel = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'context' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => $filePath, - 'assetRepo' => $this->assetRepo, - 'miscParams' => $miscParams - ] - ); - $absolutePath = '/var/www/html/magento2ce/pub/media/catalog/product'; - $hashPath = 'somehash'; - $this->context->method('getPath')->willReturn($absolutePath); - $this->encryptor->expects(static::once()) - ->method('hash') - ->with($readableParams, $this->anything()) - ->willReturn($hashPath); - static::assertEquals( - $absolutePath . '/cache/' . $hashPath . $filePath, - $imageModel->getPath() - ); - } - - /** - * @param string $filePath - * @param array $miscParams - * @param string $readableParams - * @dataProvider getPathDataProvider - */ - public function testGetUrl($filePath, $miscParams, $readableParams) - { - $imageModel = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'context' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => $filePath, - 'assetRepo' => $this->assetRepo, - 'miscParams' => $miscParams - ] - ); - $absolutePath = 'http://localhost/pub/media/catalog/product'; - $hashPath = 'somehash'; - $this->context->expects(static::once())->method('getBaseUrl')->willReturn($absolutePath); - $this->encryptor->expects(static::once()) - ->method('hash') - ->with($readableParams, $this->anything()) - ->willReturn($hashPath); - static::assertEquals( - $absolutePath . '/cache/' . $hashPath . $filePath, - $imageModel->getUrl() - ); - } - - /** - * @return array - */ - public function getPathDataProvider() - { - return [ - [ - '/some_file.png', - [], //default value for miscParams, - 'h:empty_w:empty_q:empty_r:empty_nonproportional_noframe_notransparency_notconstrainonly_nobackground', - ], - [ - '/some_file_2.png', - [ - 'image_type' => 'thumbnail', - 'image_height' => 75, - 'image_width' => 75, - 'keep_aspect_ratio' => true, - 'keep_frame' => true, - 'keep_transparency' => true, - 'constrain_only' => true, - 'background' => [233,1,0], - 'angle' => null, - 'quality' => 80, - ], - 'h:75_w:75_proportional_frame_transparency_doconstrainonly_rgb233,1,0_r:empty_q:80', - ], - [ - '/some_file_3.png', - [ - 'image_type' => 'thumbnail', - 'image_height' => 75, - 'image_width' => 75, - 'keep_aspect_ratio' => false, - 'keep_frame' => false, - 'keep_transparency' => false, - 'constrain_only' => false, - 'background' => [233,1,0], - 'angle' => 90, - 'quality' => 80, - ], - 'h:75_w:75_nonproportional_noframe_notransparency_notconstrainonly_rgb233,1,0_r:90_q:80', - ], - ]; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php index f32a7513f236b..401f16831e75a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php @@ -141,10 +141,10 @@ public function testGetUrl($imageType, $placeholderPath) if ($placeholderPath == null) { $this->imageContext->expects($this->never())->method('getBaseUrl'); - $expectedResult = 'http://localhost/pub/media/catalog/product/to_default/placeholder/by_type'; + $expectedResult = 'http://localhost/media/catalog/product/to_default/placeholder/by_type'; $this->repository->expects($this->any())->method('getUrl')->willReturn($expectedResult); } else { - $baseUrl = 'http://localhost/pub/media/catalog/product'; + $baseUrl = 'http://localhost/media/catalog/product'; $this->imageContext->expects($this->any())->method('getBaseUrl')->willReturn($baseUrl); $expectedResult = $baseUrl . DIRECTORY_SEPARATOR . $imageModel->getModule() diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 605a5e4fd5e3b..457408e0934af 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -104,9 +104,6 @@ public function testGet() ->method('create') ->willReturn($image); - $imageHelper->expects($this->once()) - ->method('getResizedImageInfo') - ->willReturn([11, 11]); $this->state->expects($this->once()) ->method('emulateAreaCode') ->with( @@ -116,12 +113,14 @@ public function testGet() ) ->willReturn($imageHelper); + $width = 5; + $height = 10; $imageHelper->expects($this->once()) ->method('getHeight') - ->willReturn(10); + ->willReturn($height); $imageHelper->expects($this->once()) ->method('getWidth') - ->willReturn(10); + ->willReturn($width); $imageHelper->expects($this->once()) ->method('getLabel') ->willReturn('Label'); @@ -137,10 +136,10 @@ public function testGet() ->with(); $image->expects($this->once()) ->method('setResizedHeight') - ->with(11); + ->with($height); $image->expects($this->once()) ->method('setResizedWidth') - ->with(11); + ->with($width); $productRenderInfoDto->expects($this->once()) ->method('setImages') diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 2324ca27ffaaf..2d4f1566a5b6e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -118,18 +118,14 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ [$product, $imageCode, (int) $productRender->getStoreId(), $image] ); - try { - $resizedInfo = $helper->getResizedImageInfo(); - } catch (NotLoadInfoImageException $exception) { - $resizedInfo = [$helper->getWidth(), $helper->getHeight()]; - } - $image->setCode($imageCode); - $image->setHeight($helper->getHeight()); - $image->setWidth($helper->getWidth()); + $height = $helper->getHeight(); + $image->setHeight($height); + $width = $helper->getWidth(); + $image->setWidth($width); $image->setLabel($helper->getLabel()); - $image->setResizedHeight($resizedInfo[1]); - $image->setResizedWidth($resizedInfo[0]); + $image->setResizedHeight($height); + $image->setResizedWidth($width); $images[] = $image; } diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 8f8a5f36e516c..4e10453f542bb 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -214,6 +214,13 @@ <source_model>Magento\Catalog\Model\Config\Source\LayoutList</source_model> </field> </group> + <group id="url"> + <field id="catalog_media_url_format" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Catalog media URL format</label> + <source_model>Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat</source_model> + <comment><![CDATA[Images should be optimized based on query parameters by your CDN or web server. Use the legacy mode for backward compatibility. <a href="https://docs.magento.com/m2/ee/user_guide/configuration/general/web.html#url-options">Learn more</a> about catalog URL formats.<br/><br/><strong style="color:red">Warning!</strong> If you switch back to legacy mode, you must <a href="https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/themes/theme-images.html#resize-catalog-images">use the CLI to regenerate images</a>.]]></comment> + </field> + </group> </section> <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> <class>separator-top</class> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index aa689c7dd35b2..b8ab4e32ec161 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -67,7 +67,8 @@ <media_storage_configuration> <allowed_resources> <tmp_images_folder>tmp</tmp_images_folder> - <catalog_product_images>media/catalog/product/cache/</catalog_product_images> + <catalog_product_images>media/catalog/product/</catalog_product_images> + <catalog_product_images_tmp>media/tmp/catalog/product/</catalog_product_images_tmp> <catalog_images_folder>catalog</catalog_images_folder> <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> @@ -83,6 +84,11 @@ <thumbnail_position>stretch</thumbnail_position> </watermark> </design> + <web> + <url> + <catalog_media_url_format>hash</catalog_media_url_format> + </url> + </web> <general> <validator_data> <input_types> diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php index ac60420713b26..617c8663d6f80 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php @@ -30,7 +30,7 @@ public function __construct( \Magento\Framework\Filesystem $filesystem ) { $this->_filesystem = $filesystem; - parent::__construct($entityFactory); + parent::__construct($entityFactory, $filesystem); } /** diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php index 33bf352adf6c5..0ba3fada2a072 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php @@ -192,12 +192,12 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) ->willReturn('localhost/index.php/'); $this->filesystemMock->expects($this->once()) ->method('getUri') - ->willReturn('pub/static'); + ->willReturn('static'); /** @var ContextInterface|MockObject $contextMock */ $contextMock = $this->getMockForAbstractClass(ContextInterface::class); $contextMock->expects($this->once()) ->method('getBaseUrl') - ->willReturn('localhost/pub/static/'); + ->willReturn('localhost/static/'); $this->assetRepoMock->expects($this->once()) ->method('getStaticViewFileContext') ->willReturn($contextMock); @@ -217,8 +217,8 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) $config = $this->wysiwygConfig->getConfig($data); $this->assertInstanceOf(DataObject::class, $config); $this->assertEquals($expectedResults[0], $config->getData('someData')); - $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticUrl')); - $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticDefaultUrl')); + $this->assertEquals('localhost/static/', $config->getData('baseStaticUrl')); + $this->assertEquals('localhost/static/', $config->getData('baseStaticDefaultUrl')); } /** diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php index e6acd431be3d5..1763a6d1800a1 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php @@ -10,6 +10,7 @@ namespace Magento\Config\Model\Config\Backend\Admin; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; /** @@ -36,7 +37,6 @@ class Robots extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param DocumentRoot $documentRoot */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,13 +46,11 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], - \Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot $documentRoot = null + array $data = [] ) { parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); - $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); - $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); + $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::PUB); $this->_file = 'robots.txt'; } diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index c16faea284296..c596f8c313ab3 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -233,7 +233,8 @@ public function moveTemporaryFile($fileName) ); } catch (\Exception $e) { throw new \Magento\Framework\Exception\LocalizedException( - __('Something went wrong while saving the file.') + __('Something went wrong while saving the file.'), + $e ); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index 62964a311af42..e1c771d79694e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -11,10 +11,13 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Model\FileProcessor; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Url\EncoderInterface; use Magento\Framework\UrlInterface; use Magento\MediaStorage\Model\File\Uploader; @@ -363,17 +366,73 @@ public function testMoveTemporaryFile() $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; $newPath = $destinationPath . $filePath; + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method('get')->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn(false); + ObjectManager::setInstance($objectManagerMock); + $this->mediaDirectory->expects($this->once()) ->method('renameFile') ->with($path, $newPath) ->willReturn(true); + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $this->assertEquals('/f/i' . $filePath, $model->moveTemporaryFile($filePath)); } + public function testMoveTemporaryFileNewFileName() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('isWritable') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with($destinationPath) + ->willReturn('/' . $destinationPath); + + $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; + + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method('get')->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturnOnConsecutiveCalls(true, true, false); + ObjectManager::setInstance($objectManagerMock); + + $this->mediaDirectory->expects($this->once()) + ->method('renameFile') + ->with($path, 'customer/f/i/filename_2.ext1') + ->willReturn(true); + + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $this->assertEquals('/f/i/filename_2.ext1', $model->moveTemporaryFile($filePath)); + } + public function testMoveTemporaryFileWithException() { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn(false); + ObjectManager::setInstance($objectManagerMock); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Something went wrong while saving the file'); diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php index 2d0018ff81ee5..816565ff7a905 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php @@ -44,7 +44,7 @@ protected function setUp(): void public function testProcess() { - $url = 'http://magento.local/pub/static/'; + $url = 'http://magento.local/static/'; $locale = 'en_US'; $css = '@import url("{{base_url_path}}frontend/_view/{{locale}}/css/email.css");'; $expectedCss = '@import url("' . $url . 'frontend/_view/' . $locale . '/css/email.css");'; diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php index fc8a0756a7b55..4946cd1092ff7 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -129,7 +129,7 @@ public function executeDataProvider(): array [ 'targetFolder' => 'media/catalog', 'type' => 'image', - 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + 'absolutePath' => 'root/media/catalog/test-image.jpeg' ] ]; } diff --git a/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php index 6a65fff7c5ebc..30d0573b62d87 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php @@ -94,7 +94,7 @@ public function testGetMediaGalleryDataJson() $data = [ [ 'media_type' => 'external-video', - 'video_url' => 'http://magento.ce/pub/media/catalog/product/9/b/9br6ujuthnc.jpg', + 'video_url' => 'http://magento.ce/media/catalog/product/9/b/9br6ujuthnc.jpg', 'is_base' => true, ], [ diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php index 519a8cba014f2..75e3efd6c599a 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php @@ -99,11 +99,20 @@ class RetrieveImageTest extends TestCase */ private $fileDriverMock; - /** - * Set up - */ + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $objectManager = new ObjectManager($this); $this->contextMock = $this->createMock(Context::class); $this->validatorMock = $this diff --git a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php new file mode 100644 index 0000000000000..59e21a9c237d0 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\MediaStorage\Model\File\Storage\Synchronization; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\Framework\Filesystem\DriverPool as LocalDriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Filesystem; + +/** + * Modifies the base URL. + */ +class MediaStorage +{ + /** + * @var bool + */ + private $isEnabled; + + /** + * @var WriteInterface + */ + private $remoteDir; + + /** + * @var WriteInterface + */ + private $localDir; + + /** + * @param Config $config + * @param Filesystem $filesystem + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct(Config $config, Filesystem $filesystem) + { + $this->isEnabled = $config->isEnabled(); + $this->remoteDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); + $this->localDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); + } + + /** + * Download remote file + * @param Synchronization $subject + * @param string $relativeFileName + * @return null + * @throws FileSystemException + * @throws ValidatorException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSynchronize(Synchronization $subject, string $relativeFileName) + { + if ($this->isEnabled) { + if ($this->remoteDir->isExist($relativeFileName)) { + $file = $this->localDir->openFile($relativeFileName, 'w'); + try { + $file->lock(); + $file->write($this->remoteDir->readFile($relativeFileName)); + $file->unlock(); + $file->close(); + } catch (FileSystemException $e) { + $file->close(); + } + } + } + return null; + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index d82c47a7caf5e..c55923f6e2109 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -9,7 +9,8 @@ "magento/module-backend": "*", "magento/module-sitemap": "*", "magento/module-cms": "*", - "magento/module-downloadable": "*" + "magento/module-downloadable": "*", + "magento/module-media-storage": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 586f07fc9ca83..fe16d1d4afca5 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -74,6 +74,9 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> + <type name="Magento\MediaStorage\Model\File\Storage\Synchronization"> + <plugin name="remote_media" type="Magento\RemoteStorage\Plugin\MediaStorage" /> + </type> <type name="Magento\Framework\Data\Collection\Filesystem"> <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml index 6c1b7f0b05a34..c06658c11ea90 100644 --- a/app/code/Magento/RemoteStorage/etc/module.xml +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -11,6 +11,7 @@ <module name="Magento_Backend"/> <module name="Magento_Sitemap"/> <module name="Magento_Store"/> + <module name="Magento_MediaStorage"/> </sequence> </module> </config> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php index 116a574b7c670..26f1f9cd6f56f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php @@ -61,7 +61,7 @@ public function testGetItems(array $products) */ public function productProvider() { - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; return [ [ [ diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index bfd2c47164cf6..866b3afd322a0 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -533,7 +533,7 @@ protected function getModelMock($mockBeforeSave = false) $methods[] = 'beforeSave'; } - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; $this->itemProviderMock->expects($this->any()) ->method('getItems') diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml index ff8087a52e42f..03cfdaaead18a 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml @@ -13,18 +13,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml index 93b9e159d4b04..f9913d5070fbd 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml @@ -31,18 +31,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php index c17e2846e22df..0622869c0b963 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php @@ -136,10 +136,10 @@ public function testGetStoreConfigs() $secureBaseUrl = 'https://magento/base_url'; $baseLinkUrl = 'http://magento/base_url/links'; $secureBaseLinkUrl = 'https://magento/base_url/links'; - $baseStaticUrl = 'http://magento/base_url/pub/static'; + $baseStaticUrl = 'http://magento/base_url/static'; $secureBaseStaticUrl = 'https://magento/base_url/static'; - $baseMediaUrl = 'http://magento/base_url/pub/media'; - $secureBaseMediaUrl = 'https://magento/base_url/pub/media'; + $baseMediaUrl = 'http://magento/base_url/media'; + $secureBaseMediaUrl = 'https://magento/base_url/media'; $locale = 'en_US'; $timeZone = 'America/Los_Angeles'; $baseCurrencyCode = 'USD'; diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index d2cd1baca894b..dd257de331b91 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -310,7 +310,7 @@ private function addFilterByParent(ProductCollection $productCollection, $parent * Method getting full media gallery for current Product * * Array structure: [ - * ['image'] => 'http://url/pub/media/catalog/product/2/0/blabla.jpg', + * ['image'] => 'http://url/media/catalog/product/2/0/blabla.jpg', * ['mediaGallery'] => [ * galleryImageId1 => simpleProductImage1.jpg, * galleryImageId2 => simpleProductImage2.jpg, diff --git a/app/code/Magento/Swatches/Helper/Media.php b/app/code/Magento/Swatches/Helper/Media.php index f3694515ecb26..bfcb354b41dfb 100644 --- a/app/code/Magento/Swatches/Helper/Media.php +++ b/app/code/Magento/Swatches/Helper/Media.php @@ -6,8 +6,9 @@ namespace Magento\Swatches\Helper; use Magento\Catalog\Helper\Image; -use Magento\Framework\App\Area; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; /** * Helper to move images from tmp to catalog directory @@ -72,6 +73,11 @@ class Media extends \Magento\Framework\App\Helper\AbstractHelper */ private $imageConfig; + /** + * @var string + */ + private $mediaUrlFormat; + /** * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem @@ -80,6 +86,8 @@ class Media extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Image\Factory $imageFactory * @param \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection * @param \Magento\Framework\View\ConfigInterface $configInterface + * @param CatalogMediaConfig $catalogMediaConfig + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Catalog\Model\Product\Media\Config $mediaConfig, @@ -88,7 +96,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Image\Factory $imageFactory, \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection, - \Magento\Framework\View\ConfigInterface $configInterface + \Magento\Framework\View\ConfigInterface $configInterface, + CatalogMediaConfig $catalogMediaConfig = null ) { $this->mediaConfig = $mediaConfig; $this->fileStorageDb = $fileStorageDb; @@ -97,6 +106,9 @@ public function __construct( $this->imageFactory = $imageFactory; $this->themeCollection = $themeCollection; $this->viewConfig = $configInterface; + + $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaUrlFormat = $catalogMediaConfig->getMediaUrlFormat(); } /** @@ -106,17 +118,35 @@ public function __construct( */ public function getSwatchAttributeImage($swatchType, $file) { - $generationPath = $swatchType . '/' . $this->getFolderNameSize($swatchType) . $file; - $absoluteImagePath = $this->mediaDirectory - ->getAbsolutePath($this->getSwatchMediaPath() . '/' . $generationPath); - if (!file_exists($absoluteImagePath)) { - try { - $this->generateSwatchVariations($file); - } catch (\Exception $e) { - return ''; + $basePath = $this->getSwatchMediaUrl(); + + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $generationPath = $swatchType . '/' . $this->getFolderNameSize($swatchType) . $file; + $absoluteImagePath = $this->mediaDirectory + ->getAbsolutePath($this->getSwatchMediaPath() . '/' . $generationPath); + if (!$this->mediaDirectory->isExist(($absoluteImagePath))) { + try { + $this->generateSwatchVariations($file); + } catch (\Exception $e) { + return ''; + } } + + return $basePath . '/' . $generationPath; } - return $this->getSwatchMediaUrl() . '/' . $generationPath; + + return $basePath . '/' . $this->getRelativeTransformationParametersPath($swatchType, $file); + } + + private function getRelativeTransformationParametersPath($swatchType, $file) + { + $imageConfig = $this->getImageConfig(); + return $this->prepareFile($file) . '?' . http_build_query([ + 'width' => $imageConfig[$swatchType]['width'], + 'height' => $imageConfig[$swatchType]['height'], + 'store' => $this->storeManager->getStore()->getCode(), + 'image-type' => $swatchType + ]); } /** @@ -156,7 +186,7 @@ public function moveImageFromTmp($file) /** * Check whether file to move exists. Getting unique name * - * @param <type> $file + * @param string $file * @return string */ protected function getUniqueFileName($file) @@ -168,13 +198,18 @@ protected function getUniqueFileName($file) ); } else { $destFile = dirname($file) . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( - $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($file)) + $this->getOriginalFilePath($file) ); } return $destFile; } + private function getOriginalFilePath($file) + { + return $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($file)); + } + /** * Generate swatch thumb and small swatch image * @@ -183,16 +218,19 @@ protected function getUniqueFileName($file) */ public function generateSwatchVariations($imageUrl) { - $absoluteImagePath = $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($imageUrl)); - foreach ($this->swatchImageTypes as $swatchType) { - $imageConfig = $this->getImageConfig(); - $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); - $image = $this->imageFactory->create($absoluteImagePath); - $this->setupImageProperties($image); - $image->resize($imageConfig[$swatchType]['width'], $imageConfig[$swatchType]['height']); - $this->setupImageProperties($image, true); - $image->save($swatchNamePath['path_for_save'], $swatchNamePath['name']); + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $absoluteImagePath = $this->getOriginalFilePath($imageUrl); + foreach ($this->swatchImageTypes as $swatchType) { + $imageConfig = $this->getImageConfig(); + $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); + $image = $this->imageFactory->create($absoluteImagePath); + $this->setupImageProperties($image); + $image->resize($imageConfig[$swatchType]['width'], $imageConfig[$swatchType]['height']); + $this->setupImageProperties($image, true); + $image->save($swatchNamePath['path_for_save'], $swatchNamePath['name']); + } } + return $this; } @@ -281,7 +319,7 @@ protected function prepareFileName($imageUrl) } /** - * Url type http://url/pub/media/attribute/swatch/ + * Url type http://url/media/attribute/swatch/ * * @return string */ diff --git a/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php b/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php index e4988bdf9308c..9e9978b499150 100644 --- a/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php @@ -7,13 +7,16 @@ namespace Magento\Swatches\Test\Unit\Helper; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product\Media\Config; use Magento\Framework\Config\View; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Image; use Magento\Framework\Image\Factory; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Store\Model\Store; @@ -59,8 +62,23 @@ class MediaTest extends TestCase /** @var Media|ObjectManager */ protected $mediaHelperObject; + /** @var CatalogMediaConfig|MockObject */ + private $catalogMediaConfigMock; + + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $objectManager = new ObjectManager($this); $this->mediaConfigMock = $this->createMock(Config::class); @@ -78,6 +96,9 @@ protected function setUp(): void $this->storeMock = $this->createPartialMock(Store::class, ['getBaseUrl']); + $this->catalogMediaConfigMock = $this->createPartialMock(CatalogMediaConfig::class, ['getMediaUrlFormat']); + $this->catalogMediaConfigMock->method('getMediaUrlFormat')->willReturn(CatalogMediaConfig::HASH); + $this->mediaDirectoryMock = $this->createMock(Write::class); $this->fileSystemMock = $this->createPartialMock(Filesystem::class, ['getDirectoryWrite']); $this->fileSystemMock @@ -94,6 +115,7 @@ protected function setUp(): void 'storeManager' => $this->storeManagerMock, 'imageFactory' => $this->imageFactoryMock, 'configInterface' => $this->viewConfigMock, + 'catalogMediaConfig' => $this->catalogMediaConfigMock, ] ); } @@ -112,7 +134,7 @@ public function testGetSwatchAttributeImage($swatchType, $expectedResult) ->expects($this->once()) ->method('getBaseUrl') ->with('media') - ->willReturn('http://url/pub/media/'); + ->willReturn('http://url/media/'); $this->generateImageConfig(); @@ -120,7 +142,7 @@ public function testGetSwatchAttributeImage($swatchType, $expectedResult) $result = $this->mediaHelperObject->getSwatchAttributeImage($swatchType, '/f/i/file.png'); - $this->assertEquals($result, $expectedResult); + $this->assertEquals($expectedResult, $result); } /** @@ -131,11 +153,11 @@ public function dataForFullPath() return [ [ 'swatch_image', - 'http://url/pub/media/attribute/swatch/swatch_image/30x20/f/i/file.png', + 'http://url/media/attribute/swatch/swatch_image/30x20/f/i/file.png', ], [ 'swatch_thumb', - 'http://url/pub/media/attribute/swatch/swatch_thumb/110x90/f/i/file.png', + 'http://url/media/attribute/swatch/swatch_thumb/110x90/f/i/file.png', ], ]; } @@ -153,6 +175,10 @@ public function testMoveImageFromTmpNoDb() { $this->fileStorageDbMock->method('checkDbUsage')->willReturn(false); $this->fileStorageDbMock->method('renameFile')->willReturnSelf(); + $this->mediaDirectoryMock + ->expects($this->atLeastOnce()) + ->method('getAbsolutePath') + ->willReturn('attribute/swatch/f/i/file.tmp'); $result = $this->mediaHelperObject->moveImageFromTmp('file.tmp'); $this->assertNotNull($result); } @@ -177,7 +203,7 @@ public function testGenerateSwatchVariations() $this->imageFactoryMock->expects($this->any())->method('create')->willReturn($image); $this->generateImageConfig(); - $image->expects($this->any())->method('resize')->willReturnSelf(); + $image->method('resize')->willReturnSelf(); $image->expects($this->atLeastOnce())->method('backgroundColor')->with([255, 255, 255])->willReturnSelf(); $this->mediaHelperObject->generateSwatchVariations('/e/a/earth.png'); } @@ -195,11 +221,11 @@ public function testGetSwatchMediaUrl() ->expects($this->once()) ->method('getBaseUrl') ->with('media') - ->willReturn('http://url/pub/media/'); + ->willReturn('http://url/media/'); $result = $this->mediaHelperObject->getSwatchMediaUrl(); - $this->assertEquals($result, 'http://url/pub/media/attribute/swatch'); + $this->assertEquals($result, 'http://url/media/attribute/swatch'); } /** @@ -282,7 +308,7 @@ protected function generateImageConfig() ], ]; - $configMock->expects($this->any())->method('getMediaEntities')->willReturn($imageConfig); + $configMock->method('getMediaEntities')->willReturn($imageConfig); } public function testGetAttributeSwatchPath() diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php index 0bbf35e244241..1978362810763 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php @@ -35,7 +35,7 @@ public function testGetLogoSrc() )->method( 'getBaseUrl' )->willReturn( - 'http://localhost/pub/media/' + 'http://localhost/media/' ); $mediaDirectory->expects($this->any())->method('isFile')->willReturn(true); @@ -53,7 +53,7 @@ public function testGetLogoSrc() ]; $block = $objectManager->getObject(Logo::class, $arguments); - $this->assertEquals('http://localhost/pub/media/logo/default/image.gif', $block->getLogoSrc()); + $this->assertEquals('http://localhost/media/logo/default/image.gif', $block->getLogoSrc()); } /** diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php index 78a56013ae042..691a94e37e932 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php @@ -195,7 +195,7 @@ public function testAfterLoad() $this->urlBuilder->expects($this->once()) ->method('getBaseUrl') ->with(['_type' => UrlInterface::URL_TYPE_MEDIA]) - ->willReturn('http://magento2.com/pub/media/'); + ->willReturn('http://magento2.com/media/'); $this->mediaDirectory->expects($this->once()) ->method('getRelativePath') ->with('value') @@ -212,7 +212,7 @@ public function testAfterLoad() $this->assertEquals( [ [ - 'url' => 'http://magento2.com/pub/media/design/file/' . $value, + 'url' => 'http://magento2.com/media/design/file/' . $value, 'file' => $value, 'size' => 234234, 'exists' => true, @@ -241,7 +241,7 @@ public function testBeforeSave(string $fileName) 'scope_id' => 1, 'value' => [ [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, + 'url' => 'http://magento2.com/media/tmp/image/' . $fileName, 'file' => $fileName, 'size' => 234234, ] @@ -314,7 +314,7 @@ public function testBeforeSaveWithExistingFile() [ 'value' => [ [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, + 'url' => 'http://magento2.com/media/tmp/image/' . $value, 'file' => $value, 'size' => 234234, 'exists' => true @@ -358,7 +358,7 @@ public function getRelativeMediaPathDataProvider(): array { return [ 'Normal path' => ['pub/media/', 'filename.jpg'], - 'Complex path' => ['some_path/pub/media/', 'filename.jpg'], + 'Complex path' => ['some_path/media/', 'filename.jpg'], ]; } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php index ab7d622801f63..c16d7a49a7e6f 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php @@ -111,7 +111,7 @@ public function testSaveToTmp() $this->store->expects($this->once()) ->method('getBaseUrl') ->with(UrlInterface::URL_TYPE_MEDIA) - ->willReturn('http://magento2.com/pub/media/'); + ->willReturn('http://magento2.com/media/'); $this->directoryWrite->expects($this->once()) ->method('getAbsolutePath') ->with('tmp/' . FileProcessor::FILE_DIR) @@ -160,7 +160,7 @@ public function testSaveToTmp() 'name' => 'file.jpg', 'size' => '234234', 'type' => 'image/jpg', - 'url' => 'http://magento2.com/pub/media/tmp/' . FileProcessor::FILE_DIR . '/file.jpg' + 'url' => 'http://magento2.com/media/tmp/' . FileProcessor::FILE_DIR . '/file.jpg' ], $this->fileProcessor->saveToTmp($fieldCode) ); diff --git a/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php b/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php index 77cf71f75ac28..0ccaf9e65b675 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php @@ -105,7 +105,7 @@ public function testGetFaviconFileNegative() public function testGetFaviconFile() { $scopeConfigValue = 'path'; - $urlToMediaDir = 'http://magento.url/pub/media/'; + $urlToMediaDir = 'http://magento.url/media/'; $expectedFile = ImageFavicon::UPLOAD_DIR . '/' . $scopeConfigValue; $expectedUrl = $urlToMediaDir . $expectedFile; diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html index 1bff60064b983..cbb00f379a655 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<img class = 'admin__control-thumbnail' data-bind="attr: {src: $data.value}"> +<img class = 'admin__control-thumbnail' style="max-height: 75px; max-width: 75px;" data-bind="attr: {src: $data.value}"> diff --git a/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php index 7afc9dc93f46e..6a23b5c66e5ba 100644 --- a/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php @@ -267,7 +267,7 @@ public function testMediaDirective() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; $this->storeMock->expects($this->once()) ->method('getBaseUrl') @@ -285,7 +285,7 @@ public function testMediaDirectiveWithEncodedQuotes() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; $this->storeMock->expects($this->once()) ->method('getBaseUrl') diff --git a/app/code/Magento/Wishlist/CustomerData/Wishlist.php b/app/code/Magento/Wishlist/CustomerData/Wishlist.php index ae54289d4b1c9..2f6b57a8650c4 100644 --- a/app/code/Magento/Wishlist/CustomerData/Wishlist.php +++ b/app/code/Magento/Wishlist/CustomerData/Wishlist.php @@ -68,7 +68,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -80,6 +80,8 @@ public function getSectionData() } /** + * Get counter + * * @return string */ protected function getCounter() @@ -156,7 +158,6 @@ protected function getItemData(\Magento\Wishlist\Model\Item $wishlistItem) * * @param \Magento\Catalog\Model\Product $product * @return array - * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function getImageData($product) { @@ -164,27 +165,11 @@ protected function getImageData($product) $helper = $this->imageHelperFactory->create() ->init($product, 'wishlist_sidebar_block'); - $template = 'Magento_Catalog/product/image_with_borders'; - - try { - $imagesize = $helper->getResizedImageInfo(); - } catch (NotLoadInfoImageException $exception) { - $imagesize = [$helper->getWidth(), $helper->getHeight()]; - } - - $width = $helper->getFrame() - ? $helper->getWidth() - : $imagesize[0]; - - $height = $helper->getFrame() - ? $helper->getHeight() - : $imagesize[1]; - return [ - 'template' => $template, + 'template' => 'Magento_Catalog/product/image_with_borders', 'src' => $helper->getUrl(), - 'width' => $width, - 'height' => $height, + 'width' => $helper->getWidth(), + 'height' => $helper->getHeight(), 'alt' => $helper->getLabel(), ]; } diff --git a/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php index 79ab3c9ba2082..0a1e40253b71c 100644 --- a/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php @@ -199,9 +199,6 @@ public function testGetSectionData() $this->catalogImageHelperMock->expects($this->any()) ->method('getFrame') ->willReturn(true); - $this->catalogImageHelperMock->expects($this->once()) - ->method('getResizedImageInfo') - ->willReturn([]); $this->wishlistHelperMock->expects($this->once()) ->method('getProductUrl') @@ -400,9 +397,6 @@ public function testGetSectionDataWithTwoItems() $this->catalogImageHelperMock->expects($this->any()) ->method('getFrame') ->willReturn(true); - $this->catalogImageHelperMock->expects($this->exactly(2)) - ->method('getResizedImageInfo') - ->willReturn([]); $this->wishlistHelperMock->expects($this->exactly(2)) ->method('getProductUrl') diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index 641253cc34c2c..dbbeaebc15936 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -649,7 +649,7 @@ public function categoryImageDataProvider(): array 'image_prefix' => '' ], 'with_pub_media_strategy' => [ - 'image_prefix' => '/pub/media/catalog/category/' + 'image_prefix' => '/media/catalog/category/' ], 'catalog_category_strategy' => [ 'image_prefix' => 'catalog/category/' diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index f086a2211b51d..b747e78651955 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -673,7 +673,7 @@ public function categoryImageDataProvider(): array 'image_prefix' => '' ], 'with_pub_media_strategy' => [ - 'image_prefix' => '/pub/media/catalog/category/' + 'image_prefix' => '/media/catalog/category/' ], 'catalog_category_strategy' => [ 'image_prefix' => 'catalog/category/' diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 7a999f1d205f2..7e94484961f9e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -73,11 +73,11 @@ public function testGetImagesJson(bool $isProductNew) $imagesJson = $this->block->getImagesJson(); $images = json_decode($imagesJson); $image = array_shift($images); - $this->assertMatchesRegularExpression('/\/m\/a\/magento_image/', $image->file); + $this->assertMatchesRegularExpression('~/m/a/magento_image~', $image->file); $this->assertSame('image', $image->media_type); $this->assertSame('Image Alt Text', $image->label); $this->assertSame('Image Alt Text', $image->label_default); - $this->assertMatchesRegularExpression('/\/pub\/media\/catalog\/product\/m\/a\/magento_image/', $image->url); + $this->assertMatchesRegularExpression('~/media/catalog/product/m/a/magento_image~', $image->url); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php index e5c6b1f8c1dd6..b57969280cdf3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -120,9 +120,23 @@ public function testGetGalleryImagesJsonWithoutImages(): void $this->assertImages(reset($result), $this->placeholderExpectation); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @magentoDbIsolation enabled + * @return void + */ + public function testGetGalleryImagesJsonWithoutImagesWithImageOptimizationParametersInUrl(): void + { + $this->block->setData('product', $this->getProduct()); + $result = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + $this->assertImages(reset($result), $this->placeholderExpectation); + } + /** * @dataProvider galleryDisabledImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation enabled * @param array $images * @param array $expectation @@ -141,6 +155,7 @@ public function testGetGalleryImagesJsonWithDisabledImage(array $images, array $ * @dataProvider galleryDisabledImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation disabled * @param array $images * @param array $expectation @@ -173,6 +188,8 @@ public function galleryDisabledImagesDataProvider(): array } /** + * Test default image generation format. + * * @dataProvider galleryImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDbIsolation enabled @@ -230,10 +247,95 @@ public function galleryImagesDataProvider(): array ]; } + /** + * @dataProvider galleryImagesWithImageOptimizationParametersInUrlDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @magentoDbIsolation enabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJsonWithImageOptimizationParametersInUrl( + array $images, + array $expectation + ): void { + $product = $this->getProduct(); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct()); + [$firstImage, $secondImage] = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + [$firstExpectedImage, $secondExpectedImage] = $expectation; + $this->assertImages($firstImage, $firstExpectedImage); + $this->assertImages($secondImage, $secondExpectedImage); + } + + /** + * @return array + */ + public function galleryImagesWithImageOptimizationParametersInUrlDataProvider(): array + { + + $imageExpectation = [ + 'thumb' => '/m/a/magento_image.jpg?width=88&height=110&store=default&image-type=thumbnail', + 'img' => '/m/a/magento_image.jpg?width=700&height=700&store=default&image-type=image', + 'full' => '/m/a/magento_image.jpg?store=default&image-type=image', + 'caption' => 'Image Alt Text', + 'position' => '1', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + $thumbnailExpectation = [ + 'thumb' => '/m/a/magento_thumbnail.jpg?width=88&height=110&store=default&image-type=thumbnail', + 'img' => '/m/a/magento_thumbnail.jpg?width=700&height=700&store=default&image-type=image', + 'full' => '/m/a/magento_thumbnail.jpg?store=default&image-type=image', + 'caption' => 'Thumbnail Image', + 'position' => '2', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + return [ + 'with_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => ['main' => true], + ], + 'expectation' => [ + $imageExpectation, + array_merge($thumbnailExpectation, ['isMain' => true]), + ], + ], + 'without_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + array_merge($imageExpectation, ['isMain' => true]), + $thumbnailExpectation, + ], + ], + 'with_changed_position' => [ + 'images' => [ + '/m/a/magento_image.jpg' => ['position' => '2'], + '/m/a/magento_thumbnail.jpg' => ['position' => '1'], + ], + 'expectation' => [ + array_merge($thumbnailExpectation, ['position' => '1']), + array_merge($imageExpectation, ['position' => '2', 'isMain' => true]), + ], + ], + ]; + } + /** * @dataProvider galleryImagesOnStoreViewDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation disabled * @param array $images * @param array $expectation diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php index b88980181fb63..283a3834eab59 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php @@ -108,7 +108,7 @@ public function uploadActionDataProvider(): array 'name' => 'magento_image.jpg', 'type' => 'image/jpeg', 'file' => '/m/a/magento_image.jpg.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.jpg', + 'url' => 'http://localhost/media/tmp/catalog/product/m/a/magento_image.jpg', 'tmp_media_path' => '/m/a/magento_image.jpg', ], ], @@ -122,7 +122,7 @@ public function uploadActionDataProvider(): array 'name' => 'product_image.png', 'type' => 'image/png', 'file' => '/p/r/product_image.png.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/p/r/product_image.png', + 'url' => 'http://localhost/media/tmp/catalog/product/p/r/product_image.png', 'tmp_media_path' => '/p/r/product_image.png', ], ], @@ -136,7 +136,7 @@ public function uploadActionDataProvider(): array 'name' => 'magento_image.gif', 'type' => 'image/gif', 'file' => '/m/a/magento_image.gif.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.gif', + 'url' => 'http://localhost/media/tmp/catalog/product/m/a/magento_image.gif', 'tmp_media_path' => '/m/a/magento_image.gif', ], ], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php index 3f9f788dc28c7..a02a2b7aeef92 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php @@ -170,7 +170,7 @@ public function testGalleryAction(): void $this->dispatch(sprintf('catalog/product/gallery/id/%s', $product->getEntityId())); $this->assertStringContainsString( - 'http://localhost/pub/media/catalog/product/', + 'http://localhost/media/catalog/product/', $this->getResponse()->getBody() ); $this->assertStringContainsString($this->getProductImageFile(), $this->getResponse()->getBody()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php index 1c9b8f2ce1918..b741285ebb6f1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php @@ -52,7 +52,7 @@ public function testSaveFilePlaceholder($model) public function testGetUrlPlaceholder($model) { $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', + 'http://localhost/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', $model->getUrl() ); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php b/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php index 46eb1e98ddc6a..7d3bf7ec1a1ea 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php @@ -82,7 +82,7 @@ public function testGetImageHtmlDeclaration( public function providerGetImageHtmlDeclaration() { return [ - [true, 'wysiwyg/hello.png', true, '<img src="http://example.com/pub/media/wysiwyg/hello.png" alt="" />'], + [true, 'wysiwyg/hello.png', true, '<img src="http://example.com/media/wysiwyg/hello.png" alt="" />'], [ false, 'wysiwyg/hello.png', @@ -96,7 +96,7 @@ function ($actualResult) { $this->assertStringContainsString($expectedResult, parse_url($actualResult, PHP_URL_PATH)); } ], - [true, 'wysiwyg/hello.png', false, 'http://example.com/pub/media/wysiwyg/hello.png'], + [true, 'wysiwyg/hello.png', false, 'http://example.com/media/wysiwyg/hello.png'], [false, 'wysiwyg/hello.png', true, '<img src="{{media url="wysiwyg/hello.png"}}" alt="" />'], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php index 3d6cbe98cf160..53b9dfee46aac 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php @@ -46,7 +46,7 @@ public function testGetConfig() public function testGetConfigCssUrls() { $config = $this->model->getConfig(); - $publicPathPattern = 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s'; + $publicPathPattern = 'http://localhost/static/%s/adminhtml/Magento/backend/en_US/%s'; $tinyMce4Config = $config->getData('tinymce4'); $contentCss = $tinyMce4Config['content_css']; if (is_array($contentCss)) { diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php index 076a669f3f8ad..7ce695cb476fe 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php @@ -105,7 +105,7 @@ public function imageDataProvider(): array true, false, 1, - '/pub/media/catalog/category/test-image.jpg' + '/media/catalog/category/test-image.jpg' ], [ 'test-image.jpg', diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php index cb96ca2a14cac..96084981fe0b8 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php @@ -109,11 +109,12 @@ public function testGetFilesCollection(): void $collection = $this->storage->getFilesCollection(self::$_baseDir, 'image'); $this->assertInstanceOf(Collection::class, $collection); foreach ($collection as $item) { + $thumbUrl = parse_url($item->getThumbUrl(), PHP_URL_PATH); $this->assertInstanceOf(DataObject::class, $item); $this->assertStringEndsWith('/' . $fileName, $item->getUrl()); $this->assertEquals( - '/pub/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', - parse_url($item->getThumbUrl(), PHP_URL_PATH), + '/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', + $thumbUrl, "Check if Thumbnail URL is equal to the generated URL" ); $this->assertEquals( @@ -387,17 +388,17 @@ public function getThumbnailUrlDataProvider(): array [ '/', 'image1.png', - '/pub/media/.thumbs/image1.png' + '/media/.thumbs/image1.png' ], [ '/cms', 'image2.png', - '/pub/media/.thumbscms/image2.png' + '/media/.thumbscms/image2.png' ], [ '/cms/pages', 'image3.png', - '/pub/media/.thumbscms/pages/image3.png' + '/media/.thumbscms/pages/image3.png' ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php index 8458a26e44659..1fd45ba1c87ba 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php @@ -6,6 +6,8 @@ namespace Magento\Config\Model\Config\Backend\Admin; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; /** * @magentoAppArea adminhtml @@ -34,10 +36,7 @@ protected function setUp(): void $this->model->setPath('design/search_engine_robots/custom_instructions'); $this->model->afterLoad(); - $documentRootPath = $objectManager->get(DocumentRoot::class)->getPath(); - $this->rootDirectory = $objectManager->get( - \Magento\Framework\Filesystem::class - )->getDirectoryRead($documentRootPath); + $this->rootDirectory = $objectManager->get(Filesystem::class)->getDirectoryRead(DirectoryList::PUB); } /** @@ -57,7 +56,8 @@ public function testAfterLoadRobotsTxtNotExists() */ public function testAfterLoadRobotsTxtExists() { - $this->assertEquals('Sitemap: http://store.com/sitemap.xml', $this->model->getValue()); + $value = $this->model->getValue(); + $this->assertEquals('Sitemap: http://store.com/sitemap.xml', $value); } /** @@ -92,7 +92,8 @@ protected function _modifyConfig() { $robotsTxt = "User-Agent: *\nDisallow: /checkout"; $this->model->setValue($robotsTxt)->save(); - $this->assertStringEqualsFile($this->rootDirectory->getAbsolutePath('robots.txt'), $robotsTxt); + $file = $this->rootDirectory->getAbsolutePath('robots.txt'); + $this->assertStringEqualsFile($file, $robotsTxt); } /** diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php b/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php index bbb229221bac3..d840261669992 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php @@ -9,7 +9,7 @@ $rootDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class )->getDirectoryWrite( - DirectoryList::ROOT + DirectoryList::PUB ); if ($rootDirectory->isExist('robots.txt')) { $rootDirectory->delete('robots.txt'); diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php b/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php index c4fb2c92c45a5..3097132b74c2c 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\TestFramework\Helper\Bootstrap; -/** @var \Magento\Framework\Filesystem\Directory\Write $rootDirectory */ -$rootDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Filesystem::class -)->getDirectoryWrite( - DirectoryList::ROOT -); -$rootDirectory->copyFile($rootDirectory->getRelativePath(__DIR__ . '/robots.txt'), 'robots.txt'); +/** @var $fileSystem Filesystem */ +$fileSystem = Bootstrap::getObjectManager()->get(Filesystem::class); +$pubDirectory = $fileSystem->getDirectoryWrite(DirectoryList::PUB); +$rootDirectory = $fileSystem->getDirectoryRead(DirectoryList::ROOT); +$source = $rootDirectory->getAbsolutePath(__DIR__ . '/robots.txt'); +$content = $rootDirectory->readFile(__DIR__ . '/robots.txt'); +$pubDirectory->writeFile('robots.txt', $content); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php deleted file mode 100644 index c6aeaf9e0f927..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Framework\Console; - -use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\DeploymentConfig\FileReader; -use Magento\Framework\App\DeploymentConfig\Writer; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Filesystem; -use Magento\Framework\ObjectManagerInterface; -use Magento\TestFramework\Helper\Bootstrap; - -class CliTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ObjectManagerInterface - */ - private $objectManager; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var ConfigFilePool - */ - private $configFilePool; - - /** - * @var FileReader - */ - private $reader; - - /** - * @var Writer - */ - private $writer; - - /** - * @var array - */ - private $envConfig; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->objectManager = Bootstrap::getObjectManager(); - $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); - $this->filesystem = $this->objectManager->get(Filesystem::class); - $this->reader = $this->objectManager->get(FileReader::class); - $this->writer = $this->objectManager->get(Writer::class); - - $this->envConfig = $this->reader->load(ConfigFilePool::APP_ENV); - } - - /** - * @inheritdoc - */ - protected function tearDown(): void - { - $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( - $this->configFilePool->getPath(ConfigFilePool::APP_ENV), - "<?php\n return array();\n" - ); - - $this->writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig], true); - } - - /** - * Checks that settings from env.php config file are applied - * to created application instance. - * - * @magentoAppIsolation enabled - * @param bool $isPub - * @param array $params - * @dataProvider documentRootIsPubProvider - */ - public function testDocumentRootIsPublic($isPub, $params) - { - $config = include __DIR__ . '/_files/env.php'; - $config['directories']['document_root_is_pub'] = $isPub; - $this->writer->saveConfig([ConfigFilePool::APP_ENV => $config], true); - - $cli = new Cli(); - $cliReflection = new \ReflectionClass($cli); - - $serviceManagerProperty = $cliReflection->getProperty('serviceManager'); - $serviceManagerProperty->setAccessible(true); - $serviceManager = $serviceManagerProperty->getValue($cli); - $deploymentConfig = $this->objectManager->get(DeploymentConfig::class); - $serviceManager->setAllowOverride(true); - $serviceManager->setService(DeploymentConfig::class, $deploymentConfig); - $serviceManagerProperty->setAccessible(false); - - $documentRootResolver = $cliReflection->getMethod('documentRootResolver'); - $documentRootResolver->setAccessible(true); - - self::assertEquals($params, $documentRootResolver->invoke($cli)); - } - - /** - * Provides document root setting and expecting - * properties for object manager creation. - * - * @return array - */ - public function documentRootIsPubProvider(): array - { - return [ - [true, [ - 'MAGE_DIRS' => [ - 'pub' => ['uri' => ''], - 'media' => ['uri' => 'media'], - 'static' => ['uri' => 'static'], - 'upload' => ['uri' => 'media/upload'] - ] - ]], - [false, []] - ]; - } -} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php b/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php deleted file mode 100644 index e314e7638c22c..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -return [ - 'backend' => [ - 'frontName' => 'admin', - ], - 'crypt' => [ - 'key' => 'some_key', - ], - 'session' => [ - 'save' => 'files', - ], - 'db' => [ - 'table_prefix' => '', - 'connection' => [], - ], - 'resource' => [], - 'x-frame-options' => 'SAMEORIGIN', - 'MAGE_MODE' => 'default', - 'cache_types' => [ - 'config' => 1, - 'layout' => 1, - 'block_html' => 1, - 'collections' => 1, - 'reflection' => 1, - 'db_ddl' => 1, - 'eav' => 1, - 'customer_notification' => 1, - 'config_integration' => 1, - 'config_integration_api' => 1, - 'full_page' => 1, - 'translate' => 1, - 'config_webservice' => 1, - ], - 'install' => [ - 'date' => 'Thu, 09 Feb 2017 14:28:00 +0000', - ], - 'directories' => [ - 'document_root_is_pub' => true - ] -]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html index d5b6f35421ac6..ade4f52d5153f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html @@ -30,7 +30,7 @@ height="52" - src="http://magento2.vagrant236/pub/static/version1502812784/frontend/Magento/blank/en_US/Magento_Email/logo_email.png" + src="http://magento2.vagrant236/static/version1502812784/frontend/Magento/blank/en_US/Magento_Email/logo_email.png" alt="Main Website Store" border="0" /> diff --git a/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html b/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html index 573f3b166db35..0afba67d3b031 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html @@ -14,12 +14,12 @@ <div data-translate="[{"shown":"shown_1","translated":"translated_1","original":"original_1","location":"Tag attribute (ALT, TITLE, etc.)","scope":"scope_1"}]"title="some_title_shown_1_in_div"> some_text_<span data-translate="[{"shown":"shown_2","translated":"translated_2","original":"original_2","location":"Text","scope":"scope_2"}]">shown_2</span>_in_div </div> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/prototype/window.js"></script> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/static/frontend/Magento/luma/en_US/prototype/windows/themes/default.css"/> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/prototype/magento.css"/> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/edit-trigger.js"></script> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/translate-inline.js"></script> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/translate-inline.css"/> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/prototype/window.js"></script> +<link rel="stylesheet" type="text/css" href="http://localhost/static/frontend/Magento/luma/en_US/prototype/windows/themes/default.css"/> +<link rel="stylesheet" type="text/css" href="http://localhost/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/prototype/magento.css"/> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/mage/edit-trigger.js"></script> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/mage/translate-inline.js"></script> +<link rel="stylesheet" type="text/css" href="http://localhost/static/frontend/Magento/luma/en_US/mage/translate-inline.css"/> <script type="text/javascript"> (function($){ @@ -27,7 +27,7 @@ $(this).translateInline({ ajaxUrl: 'http://localhost/index.php/translation/ajax/index/', area: 'frontend', - editTrigger: {img: 'http://localhost/pub/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/fam_book_open.png'} + editTrigger: {img: 'http://localhost/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/fam_book_open.png'} }); }); })(jQuery); diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index 785637a9470cb..ad4491b166cfe 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -181,7 +181,7 @@ public function testGetBaseUrlWithTypeRestoring() * Get url with type specified in params */ $mediaUrl = $this->model->getBaseUrl(['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA]); - $this->assertEquals('http://localhost/pub/media/', $mediaUrl, 'Incorrect media url'); + $this->assertEquals('http://localhost/media/', $mediaUrl, 'Incorrect media url'); $this->assertEquals('http://localhost/index.php/', $this->model->getBaseUrl(), 'Incorrect link url'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php index fa664756d65f1..f584b8f7cfcd3 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php @@ -490,7 +490,7 @@ public function testGetViewFileUrl() { $actualResult = $this->_block->getViewFileUrl('css/styles.css'); $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/frontend/%s/en_US/css/styles.css', + 'http://localhost/static/%s/frontend/%s/en_US/css/styles.css', $actualResult ); } diff --git a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php index 4dfe01eed2d01..3120cf399d96c 100644 --- a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php @@ -17,8 +17,8 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * Base product image path */ - const BASE_IMAGE_PATH = '#http\:\/\/localhost\/pub\/media\/catalog\/product\/cache\/[a-z0-9]{32}:path:#'; - + const BASE_IMAGE_PATH = '#http://localhost/media/catalog/product/cache/[a-z0-9]{32}:path:#'; + /** * Test getCollection None images * 1) Check that image attributes were not loaded @@ -52,6 +52,7 @@ public function testGetCollectionNone() * 3) Check thumbnails when no thumbnail selected * * @magentoConfigFixture default_store sitemap/product/image_include all + * @magentoConfigFixture default/web/url/catalog_media_url_format hash */ public function testGetCollectionAll() { @@ -120,6 +121,7 @@ public function testGetCollectionAll() * 3) Check thumbnails when no thumbnail selected * * @magentoConfigFixture default_store sitemap/product/image_include base + * @magentoConfigFixture default/web/url/catalog_media_url_format hash */ public function testGetCollectionBase() { diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index 3f7c3c5a9a452..d81a6fa52ea48 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -169,14 +169,14 @@ public function getBaseUrlDataProvider() [UrlInterface::URL_TYPE_DIRECT_LINK, false, true, 'http://localhost/index.php/'], [UrlInterface::URL_TYPE_DIRECT_LINK, true, false, 'http://localhost/'], [UrlInterface::URL_TYPE_DIRECT_LINK, true, true, 'http://localhost/'], - [UrlInterface::URL_TYPE_STATIC, false, false, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, false, true, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, true, false, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, true, true, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_MEDIA, false, false, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, false, true, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, true, false, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, true, true, 'http://localhost/pub/media/'] + [UrlInterface::URL_TYPE_STATIC, false, false, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, false, true, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, true, false, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, true, true, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_MEDIA, false, false, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, false, true, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, true, false, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, true, true, 'http://localhost/media/'] ]; } @@ -196,8 +196,8 @@ public function testGetBaseUrlInPub() $this->model = $this->_getStoreModel(); $this->model->load('default'); - $this->assertEquals('http://localhost/pub/static/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_STATIC)); - $this->assertEquals('http://localhost/pub/media/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)); + $this->assertEquals('http://localhost/static/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_STATIC)); + $this->assertEquals('http://localhost/media/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)); } /** diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php index fc3b0399d0497..d4a14420c4ae6 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php @@ -11,7 +11,7 @@ public function testMediaDirective() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; /** @var \Magento\Widget\Model\Template\Filter $filter */ $filter = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -25,7 +25,7 @@ public function testMediaDirectiveWithEncodedQuotes() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; /** @var \Magento\Widget\Model\Template\Filter $filter */ $filter = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php index a9d21ec84e32b..fb13ea57475ad 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php @@ -46,7 +46,7 @@ public function testGetPluginSettings() $jsFilename = $plugins['src']; $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', + 'http://localhost/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', $jsFilename ); diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php index 9507e50d71638..43aacecb6982e 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php @@ -125,7 +125,7 @@ public function splitQuote() ); $command = $this->getCliScriptCommand() . ' setup:db-schema:split-quote ' . implode(" ", array_keys($installParams)) . - ' -vvv --magento-init-params="' . + ' -vvv --no-interaction --magento-init-params="' . $initParams['magento-init-params'] . '"'; $this->shell->execute($command, array_values($installParams)); diff --git a/index.php b/index.php deleted file mode 100644 index 9ac7f6ffa71b2..0000000000000 --- a/index.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Application entry point - * - * Example - run a particular store or website: - * -------------------------------------------- - * require __DIR__ . '/app/bootstrap.php'; - * $params = $_SERVER; - * $params[\Magento\Store\Model\StoreManager::PARAM_RUN_CODE] = 'website2'; - * $params[\Magento\Store\Model\StoreManager::PARAM_RUN_TYPE] = 'website'; - * $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params); - * \/** @var \Magento\Framework\App\Http $app *\/ - * $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); - * $bootstrap->run($app); - * -------------------------------------------- - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -try { - require __DIR__ . '/app/bootstrap.php'; -} catch (\Exception $e) { - echo <<<HTML -<div style="font:12px/1.35em arial, helvetica, sans-serif;"> - <div style="margin:0 0 25px 0; border-bottom:1px solid #ccc;"> - <h3 style="margin:0;font-size:1.7em;font-weight:normal;text-transform:none;text-align:left;color:#2f2f2f;"> - Autoload error</h3> - </div> - <p>{$e->getMessage()}</p> -</div> -HTML; - exit(1); -} - -$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); -/** @var \Magento\Framework\App\Http $app */ -$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); -$bootstrap->run($app); diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index 6caf2c0f88dfa..295ac50cf5687 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -157,12 +157,12 @@ public static function getDefaultConfig() self::DI => [parent::PATH => 'generated/metadata'], self::GENERATION => [parent::PATH => Io::DEFAULT_DIRECTORY], self::SESSION => [parent::PATH => 'var/session'], - self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'pub/media'], - self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'pub/static'], - self::PUB => [parent::PATH => 'pub', parent::URL_PATH => 'pub'], + self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'media'], + self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'static'], + self::PUB => [parent::PATH => 'pub', parent::URL_PATH => ''], self::LIB_WEB => [parent::PATH => 'lib/web'], self::TMP => [parent::PATH => 'var/tmp'], - self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'pub/media/upload'], + self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'media/upload'], self::TMP_MATERIALIZATION_DIR => [parent::PATH => 'var/view_preprocessed/pub/static'], self::TEMPLATE_MINIFICATION_DIR => [parent::PATH => 'var/view_preprocessed'], self::SETUP => [parent::PATH => 'setup/src'], diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php deleted file mode 100644 index 90c32b54f17c5..0000000000000 --- a/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Framework\App\Test\Unit\Config; - -use Magento\Framework\App\Config; -use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\ConfigOptionsListConstants; -use Magento\Framework\Config\DocumentRoot; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test class for checking settings that defined in config file - */ -class DocumentRootTest extends TestCase -{ - /** - * @var Config|MockObject - */ - private $configMock; - - /** - * @var DocumentRoot - */ - private $documentRoot; - - protected function setUp(): void - { - $this->configMock = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->documentRoot = new DocumentRoot($this->configMock); - } - - /** - * Ensures that the path returned matches the pub/ path. - */ - public function testGetPath() - { - $this->configMockSetForDocumentRootIsPub(); - - $this->assertSame(DirectoryList::PUB, $this->documentRoot->getPath()); - } - - /** - * Ensures that the deployment configuration returns the mocked value for - * the pub/ folder. - */ - public function testIsPub() - { - $this->configMockSetForDocumentRootIsPub(); - - $this->assertTrue($this->documentRoot->isPub()); - } - - private function configMockSetForDocumentRootIsPub() - { - $this->configMock->expects($this->any()) - ->method('get') - ->willReturnMap([ - [ - ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, - null, - true - ], - ]); - } -} diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php index 45ccc34f0ce5b..363a48d822ace 100644 --- a/lib/internal/Magento/Framework/Config/DocumentRoot.php +++ b/lib/internal/Magento/Framework/Config/DocumentRoot.php @@ -10,7 +10,7 @@ /** * Document root detector. - * + * @deprecared Magento always uses the pub directory * @api */ class DocumentRoot @@ -35,7 +35,7 @@ public function __construct(DeploymentConfig $config) */ public function getPath(): string { - return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + return DirectoryList::PUB; } /** @@ -45,6 +45,6 @@ public function getPath(): string */ public function isPub(): bool { - return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + return true; } } diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index f22c452549a78..c7192e7dfbb33 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -174,7 +174,6 @@ private function initObjectManager() { $params = (new ComplexParameter(self::INPUT_KEY_BOOTSTRAP))->mergeFromArgv($_SERVER, $_SERVER); $params[Bootstrap::PARAM_REQUIRE_MAINTENANCE] = null; - $params = $this->documentRootResolver($params); $requestParams = $this->serviceManager->get('magento-init-params'); $appBootstrapKey = Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS; @@ -230,26 +229,4 @@ protected function getVendorCommands($objectManager) return array_merge([], ...$commands); } - - /** - * Provides updated configuration in accordance to document root settings. - * - * @param array $config - * @return array - */ - private function documentRootResolver(array $config = []): array - { - $params = []; - $deploymentConfig = $this->serviceManager->get(DeploymentConfig::class); - if ((bool)$deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB)) { - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ]; - } - - return array_merge_recursive($config, $params); - } } diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 767cda60c0d35..be00cb9f64c18 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -7,10 +7,12 @@ namespace Magento\Framework\Data\Collection; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Data\Collection; -use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Phrase; /** * Filesystem items collection @@ -130,28 +132,26 @@ class Filesystem extends \Magento\Framework\Data\Collection protected $_collectedFiles = []; /** - * @var TargetDirectory|null + * @var \Magento\Framework\Filesystem */ - private $targetDirectory; + private $filesystem; /** - * @var DocumentRoot|null + * @var WriteInterface */ - private $documentRoot; + private $rootDirectory; /** * @param EntityFactoryInterface|null $_entityFactory - * @param TargetDirectory|null $targetDirectory - * @param DocumentRoot|null $documentRoot + * @param \Magento\Framework\Filesystem $filesystem */ public function __construct( EntityFactoryInterface $_entityFactory = null, - TargetDirectory $targetDirectory = null, - DocumentRoot $documentRoot = null + \Magento\Framework\Filesystem $filesystem = null ) { $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); - $this->targetDirectory = $targetDirectory ?? ObjectManager::getInstance()->get(TargetDirectory::class); - $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class); + $this->filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); + $this->rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); parent::__construct($this->_entityFactory); } @@ -237,11 +237,8 @@ public function setCollectRecursively($value) public function addTargetDir($value) { $value = (string)$value; - $directory = $this->targetDirectory->getDirectoryWrite($this->documentRoot->getPath()); - - if (!$directory->isDirectory($value)) { - // phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception('Unable to set target directory.'); + if (!$this->rootDirectory->isDirectory($value)) { + throw new FileSystemException(__('Unable to set target directory.')); } $this->_targetDirs[$value] = $value; return $this; @@ -266,19 +263,18 @@ public function setDirsFirst($value) * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException */ protected function _collectRecursive($dir) { - $directory = $this->targetDirectory->getDirectoryRead($this->documentRoot->getPath()); $collectedResult = []; if (!is_array($dir)) { $dir = [$dir]; } foreach ($dir as $folder) { - if ($nodes = $directory->search('/*', $folder)) { + if ($nodes = $this->rootDirectory->search('/*', $folder)) { foreach ($nodes as $node) { - $collectedResult[] = $directory->getAbsolutePath($node); + $collectedResult[] = $this->rootDirectory->getAbsolutePath($node); } } } @@ -287,7 +283,7 @@ protected function _collectRecursive($dir) } foreach ($collectedResult as $item) { - if ($directory->isDirectory($item) + if ($this->rootDirectory->isDirectory($item) && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item))) ) { if ($this->_collectDirs) { @@ -300,7 +296,7 @@ protected function _collectRecursive($dir) if ($this->_collectRecursively) { $this->_collectRecursive($item); } - } elseif ($this->_collectFiles && $directory->isFile( + } elseif ($this->_collectFiles && $this->rootDirectory->isFile( $item ) && (!$this->_allowedFilesMask || preg_match( $this->_allowedFilesMask, diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index b944ceb94628b..706d6efef44b9 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; @@ -46,8 +47,7 @@ class Uploader /** * Upload type. Used to right handle $_FILES array. - * - * @var \Magento\Framework\File\Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE + * @var Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE * @access protected */ protected $_uploadType; @@ -212,7 +212,7 @@ public function __construct( TargetDirectory $targetDirectory = null, DocumentRoot $documentRoot = null ) { - $this->directoryList= $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); + $this->directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); $this->_setUploadFileId($fileId); if (!file_exists($this->_file['tmp_name'])) { @@ -741,7 +741,7 @@ private function validateFileId(array $fileId): void * Create destination folder * * @param string $destinationFolder - * @return \Magento\Framework\File\Uploader + * @return Uploader * @throws FileSystemException */ private function createDestinationFolder(string $destinationFolder) @@ -774,20 +774,24 @@ private function createDestinationFolder(string $destinationFolder) */ public static function getNewFileName($destinationFile) { + /** @var Filesystem $fileSystem */ + $fileSystem = ObjectManager::getInstance()->get(Filesystem::class); + $local = $fileSystem->getDirectoryRead(DirectoryList::ROOT); + /** @var TargetDirectory $targetDirectory */ + $targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + $remote = $targetDirectory->getDirectoryRead(DirectoryList::ROOT); + + $fileExists = function ($path) use ($local, $remote) { + return $local->isExist($path) || $remote->isExist($path); + }; + $fileInfo = pathinfo($destinationFile); - if (file_exists($destinationFile)) { - $index = 1; - $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; - while (file_exists($fileInfo['dirname'] . '/' . $baseName)) { - $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; - $index++; - } - $destFileName = $baseName; - } else { - return $fileInfo['basename']; + $index = 1; + while ($fileExists($fileInfo['dirname'] . '/' . $fileInfo['basename'])) { + $fileInfo['basename'] = $fileInfo['filename'] . '_' . $index++ . '.' . $fileInfo['extension']; } - return $destFileName; + return $fileInfo['basename']; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 1d60b7ce879bf..0ff25f868d7af 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -116,7 +116,7 @@ public function renameFile($path, $newPath, WriteInterface $targetDirectory = nu } $absolutePath = $this->driver->getAbsolutePath($this->path, $path); $absoluteNewPath = $targetDirectory->getAbsolutePath($newPath); - return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->driver); + return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->getDriver()); } /** diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index 13d6e7b72d89f..fab7eb93aabf8 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -5,6 +5,7 @@ */ namespace Magento\Framework\HTTP\PhpEnvironment; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; use Magento\Framework\Stdlib\StringUtils; use Laminas\Http\Header\HeaderInterface; @@ -794,7 +795,7 @@ public function setRequestUri($requestUri = null) public function getBaseUrl() { $url = urldecode(parent::getBaseUrl()); - $url = str_replace('\\', '/', $url); + $url = str_replace(['\\', '/' . DirectoryList::PUB .'/'], '/', $url); return $url; } diff --git a/lib/internal/Magento/Framework/Image.php b/lib/internal/Magento/Framework/Image.php index b3867c0197b79..a14f94b8f2733 100644 --- a/lib/internal/Magento/Framework/Image.php +++ b/lib/internal/Magento/Framework/Image.php @@ -48,10 +48,6 @@ public function open() { $this->_adapter->checkDependencies(); - if (!file_exists($this->_fileName)) { - throw new \Exception("File '{$this->_fileName}' does not exist."); - } - $this->_adapter->open($this->_fileName); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php index 48935913e0561..76c659791308e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php @@ -13,8 +13,10 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Image\Factory; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Design\Theme\Image; use Magento\Framework\View\Design\Theme\Image\Uploader; @@ -70,8 +72,20 @@ class ImageTest extends TestCase */ protected $imagePathMock; + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $this->_mediaDirectoryMock = $this->createPartialMock( Write::class, ['isExist', 'copyFile', 'getRelativePath', 'delete'] diff --git a/nginx.conf.sample b/nginx.conf.sample index ead80ccb22ece..296f9fafd0a35 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -26,6 +26,9 @@ ## ## In production mode, you should uncomment the 'expires' directive in the /static/ location block +# Modules can be loaded only at the very beginning of the Nginx config file, please move the line below to the main config file +# load_module /etc/nginx/modules/ngx_http_image_filter_module.so; + root $MAGE_ROOT/pub; index index.php; @@ -134,6 +137,28 @@ location /static/ { } location /media/ { + +## The following section allows to offload image resizing from Magento instance to the Nginx. +## Catalog image URL format should be set accordingly. +## See https://docs.magento.com/m2/ee/user_guide/configuration/general/web.html#url-options +# location ~* ^/media/catalog/.* { +# +# # Replace placeholders and uncomment the line below to serve product images from public S3 +# # See examples of S3 authentication at https://github.com/anomalizer/ngx_aws_auth +# # proxy_pass https://<bucket-name>.<region-name>.amazonaws.com; +# +# set $width "-"; +# set $height "-"; +# if ($arg_width != '') { +# set $width $arg_width; +# } +# if ($arg_height != '') { +# set $height $arg_height; +# } +# image_filter resize $width $height; +# image_filter_jpeg_quality 90; +# } + try_files $uri $uri/ /get.php$is_args$args; location ~ ^/media/theme_customization/.*\.xml { diff --git a/pub/.htaccess b/pub/.htaccess index 6a97a6d14dc00..d30951ee22ca5 100644 --- a/pub/.htaccess +++ b/pub/.htaccess @@ -22,6 +22,11 @@ ## cgi.fix_pathinfo = 1 ## If it still doesn't work, rename php.ini to php5.ini +############################################ +## Enable usage of methods arguments in backtrace + + #SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## This line is specific for 1and1 hosting @@ -33,24 +38,6 @@ DirectoryIndex index.php -<IfModule mod_php5.c> -############################################ -## Adjust memory limit - - php_value memory_limit 756M - php_value max_execution_time 18000 - -############################################ -## Disable automatic session start -## before autoload was initialized - - php_flag session.auto_start off - -############################################ -# Disable user agent verification to not break multiple image upload - - php_flag suhosin.session.cryptua off -</IfModule> <IfModule mod_php7.c> ############################################ ## Adjust memory limit @@ -75,7 +62,6 @@ php_flag suhosin.session.cryptua off </IfModule> - <IfModule mod_security.c> ########################################### # Disable POST processing to not break multiple image upload @@ -93,7 +79,7 @@ # Insert filter on all content ###SetOutputFilter DEFLATE # Insert filter on selected content types only - #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript + #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/x-javascript application/json image/svg+xml # Netscape 4.x has some problems... #BrowserMatch ^Mozilla/4 gzip-only-text/html @@ -121,6 +107,13 @@ </IfModule> +############################################ +## Workaround for Apache 2.4.6 CentOS build when working via ProxyPassMatch with HHVM (or any other) +## Please, set it on virtual host configuration level + +## SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +############################################ + <IfModule mod_rewrite.c> ############################################ @@ -147,6 +140,13 @@ RewriteCond %{REQUEST_METHOD} ^TRAC[EK] RewriteRule .* - [L,R=405] +############################################ +## Redirect for mobile user agents + + #RewriteCond %{REQUEST_URI} !^/mobiledirectoryhere/.*$ + #RewriteCond %{HTTP_USER_AGENT} "android|blackberry|ipad|iphone|ipod|iemobile|opera mobile|palmos|webos|googlebot-mobile" [NC] + #RewriteRule ^(.*)$ /mobiledirectoryhere/ [L,R=302] + ############################################ ## Never rewrite for existing files, directories and links @@ -168,6 +168,7 @@ AddDefaultCharset Off #AddDefaultCharset UTF-8 + AddType 'text/html; charset=UTF-8' html <IfModule mod_expires.c> @@ -193,18 +194,15 @@ Require all denied </IfVersion> </Files> - -# For 404s and 403s that aren't handled by the application, show plain 404 response -ErrorDocument 404 /errors/404.php -ErrorDocument 403 /errors/404.php - -############################################ -## If running in cluster environment, uncomment this -## http://developer.yahoo.com/performance/rules.html#etags - - #FileETag none - -########################################### + <Files .htaccess> + <IfVersion < 2.4> + order allow,deny + deny from all + </IfVersion> + <IfVersion >= 2.4> + Require all denied + </IfVersion> + </Files> ## Deny access to cron.php <Files cron.php> <IfVersion < 2.4> @@ -226,8 +224,48 @@ ErrorDocument 403 /errors/404.php </IfVersion> </Files> +# For 404s and 403s that aren't handled by the application, show plain 404 response +ErrorDocument 404 /errors/404.php +ErrorDocument 403 /errors/404.php + +################################ +## If running in cluster environment, uncomment this +## http://developer.yahoo.com/performance/rules.html#etags + + #FileETag none + +# ###################################################################### +# # INTERNET EXPLORER # +# ###################################################################### + +# ---------------------------------------------------------------------- +# | Document modes | +# ---------------------------------------------------------------------- + +# Force Internet Explorer 8/9/10 to render pages in the highest mode +# available in the various cases when it may not. +# +# https://hsivonen.fi/doctype/#ie8 +# +# (!) Starting with Internet Explorer 11, document modes are deprecated. +# If your business still relies on older web apps and services that were +# designed for older versions of Internet Explorer, you might want to +# consider enabling `Enterprise Mode` throughout your company. +# +# https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode +# http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx + <IfModule mod_headers.c> ############################################ + Header set X-UA-Compatible "IE=edge" + + # `mod_headers` cannot match based on the content-type, however, + # the `X-UA-Compatible` response header should be send only for + # HTML documents and not for the other resources. + <FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$"> + Header unset X-UA-Compatible + </FilesMatch> + ## Prevent clickjacking Header set X-Frame-Options SAMEORIGIN </IfModule> diff --git a/pub/index.php b/pub/index.php index 612e190719053..9e91f3bfa5488 100644 --- a/pub/index.php +++ b/pub/index.php @@ -7,7 +7,6 @@ */ use Magento\Framework\App\Bootstrap; -use Magento\Framework\App\Filesystem\DirectoryList; try { require __DIR__ . '/../app/bootstrap.php'; @@ -24,17 +23,7 @@ exit(1); } -$params = $_SERVER; -$params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = array_replace_recursive( - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] ?? [], - [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ] -); -$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params); +$bootstrap = Bootstrap::create(BP, $_SERVER); /** @var \Magento\Framework\App\Http $app */ $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); $bootstrap->run($app); diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index cfcdebd4ac373..9b42548c4e105 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -37,7 +37,7 @@ public function __construct( /** * Generates image from $data and puts its to /tmp folder * - * @param string $config + * @param array $config * @return string $imagePath */ public function generate($config) diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php index e838dbee33603..0e9cc65f17bd9 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php @@ -15,6 +15,7 @@ /** * Deployment configuration options for the folders. + * @deprecared Magento always uses the pub directory */ class Directory implements ConfigOptionsListInterface { @@ -70,7 +71,7 @@ public function getOptions() $this->selectOptions, self::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, 'Flag to show is Pub is on root, can be true or false only', - false + true ), ]; } From b336df77b174950ce189d79600601bde8de78fd7 Mon Sep 17 00:00:00 2001 From: rrego6 <rrego@adobe.com> Date: Thu, 22 Oct 2020 19:19:22 -0400 Subject: [PATCH 097/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Added further test cases --- .../TestFramework/Dependency/PhpRule.php | 12 +- .../TestFramework/Dependency/PhpRuleTest.php | 150 +++++++++++++++--- 2 files changed, 135 insertions(+), 27 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 53401f0b47f04..88f3c17203120 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -106,6 +106,7 @@ class PhpRule implements RuleInterface * @param array $pluginMap * @param array $whitelists * @param ClassScanner|null $classScanner + * @param RouteMapper|null $routeMapper */ public function __construct( array $mapRouters, @@ -113,15 +114,16 @@ public function __construct( ConfigReader $configReader, array $pluginMap = [], array $whitelists = [], - ClassScanner $classScanner = null + ClassScanner $classScanner = null, + RouteMapper $routeMapper = null ) { $this->_mapRouters = $mapRouters; $this->_mapLayoutBlocks = $mapLayoutBlocks; $this->configReader = $configReader; $this->pluginMap = $pluginMap ?: null; - $this->routeMapper = new RouteMapper(); $this->whitelists = $whitelists; $this->classScanner = $classScanner ?? new ClassScanner(); + $this->routeMapper = $routeMapper ?? new RouteMapper(); } /** @@ -388,9 +390,9 @@ private function processWildcardUrl(string $urlPath, string $filePath) } return $this->routeMapper->getDependencyByRoutePath( - $routeId, - $controllerName, - $actionName + strtolower($routeId), + strtolower($controllerName), + strtolower($actionName) ); } diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 74c8bd97c7eb1..ec18a58245c1b 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -10,6 +10,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\TestFramework\Dependency\Reader\ClassScanner; +use Magento\TestFramework\Dependency\Route\RouteMapper; /** * Test for PhpRule dependency check @@ -36,34 +37,36 @@ class PhpRuleTest extends \PHPUnit\Framework\TestCase */ private $webApiConfigReader; + private $pluginMap; + private $mapRoutes; + private $mapLayoutBlocks; + private $whitelist; + /** * @inheritDoc * @throws \Exception */ protected function setUp(): void { - $mapRoutes = ['someModule' => ['Magento\SomeModule'], 'anotherModule' => ['Magento\OneModule']]; - $mapLayoutBlocks = ['area' => ['block.name' => ['Magento\SomeModule' => 'Magento\SomeModule']]]; - $pluginMap = [ + $this->mapRoutes = ['someModule' => ['Magento\SomeModule'], 'anotherModule' => ['Magento\OneModule']]; + $this->mapLayoutBlocks = ['area' => ['block.name' => ['Magento\SomeModule' => 'Magento\SomeModule']]]; + $this->pluginMap = [ 'Magento\Module1\Plugin1' => 'Magento\Module1\Subject', 'Magento\Module1\Plugin2' => 'Magento\Module2\Subject', ]; - $whitelist = []; + $this->whitelist = []; $this->objectManagerHelper = new ObjectManagerHelper($this); $this->classScanner = $this->createMock(ClassScanner::class); $this->webApiConfigReader = $this->makeWebApiConfigReaderMock(); - $this->model = $this->objectManagerHelper->getObject( - PhpRule::class, - [ - 'mapRouters' => $mapRoutes, - 'mapLayoutBlocks' => $mapLayoutBlocks, - 'pluginMap' => $pluginMap, - 'configReader' => $this->webApiConfigReader, - 'whitelists' => $whitelist, - 'classScanner' => $this->classScanner - ] + $this->model = new PhpRule( + $this->mapRoutes, + $this->mapLayoutBlocks, + $this->webApiConfigReader, + $this->pluginMap, + $this->whitelist, + $this->classScanner ); } @@ -225,6 +228,7 @@ public function getDependencyInfoDataProvider() ]; } + /** * @param string $class * @param string $content @@ -265,12 +269,17 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() ] ] ], - 'getUrl from API of same module' => [ + 'getUrl from API of same module with parameter' => [ 'Magento\Catalog\SomeClass', '$this->getUrl("rest/V1/products/3")', [] ], - 'getUrl from API of different module' => [ + 'getUrl from API of same module without parameter' => [ + 'Magento\Catalog\SomeClass', + '$this->getUrl("rest/V1/products")', + [] + ], + 'getUrl from API of different module with parameter' => [ 'Magento\Backend\SomeClass', '$this->getUrl("rest/V1/products/43/options")', [ @@ -281,7 +290,7 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() ] ], ], - 'getUrl from routeid wildcard in controller' => [ + 'getUrl from routeid wildcard' => [ 'Magento\Catalog\Controller\ControllerName\SomeClass', '$this->getUrl("*/Invalid/*")', [] @@ -291,19 +300,112 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() '$this->getUrl("Catalog/*/View")', [] ], - 'getUrl from wildcard url within ignored Block class' => [ - 'Magento\Cms\Block\SomeClass', + 'getUrl from wildcard url within ignored Model file' => [ + 'Magento\Cms\Model\SomeClass', '$this->getUrl("Catalog/*/View")', [] ], - 'getUrl from wildcard url for ControllerName' => [ - 'Magento\Catalog\Controller\Category\IGNORE', - '$this->getUrl("Catalog/*/View")', + 'getUrl with in admin controller for controllerName wildcard' => [ + 'Magento\Backend\Controller\Adminhtml\System\Store\DeleteStore', + '$this->getUrl("adminhtml/*/deleteStorePost")', [] ], ]; } + + /** + * @param string $template + * @param string $content + * @param array $expected + * @throws \Exception + * @dataProvider getDependencyInfoDataCaseGetTemplateUrlDataProvider + */ + public function testGetDependencyInfoCaseTemplateGetUrl( + string $template, + string $content, + array $expected + ) { + $module = $this->getModuleFromClass($template); + + $this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $template, $content)); + } + + public function getDependencyInfoDataCaseGetTemplateUrlDataProvider() + { + return [ 'getUrl from ignore template' => [ + 'app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar/script.phtml', + '$getUrl("adminhtml/*/ajaxBlock"', + []]]; + } + + /** + * @param string $class + * @param string $content + * @param array $expected + * @dataProvider processWildcardUrlDataProvider + */ + public function testProcessWildcardUrl( + string $class, + string $content, + array $expected + ) { + $routeMapper = $this->createMock(RouteMapper::class); + $routeMapper->expects($this->once()) + ->method('getDependencyByRoutePath') + ->with( + $this->equalTo($expected['route_id']), + $this->equalTo($expected['controller_name']), + $this->equalTo($expected['action_name']) + ); + $phpRule = new PhpRule( + $this->mapRoutes, + $this->mapLayoutBlocks, + $this->webApiConfigReader, + $this->pluginMap, + $this->whitelist, + $this->classScanner, + $routeMapper + ); + $file = $this->makeMockFilepath($class); + $module = $this->getModuleFromClass($class); + + $phpRule->getDependencyInfo($module, 'php', $file, $content); + } + + public function processWildcardUrlDataProvider() + { + return [ + 'wildcard controller route' => [ + 'Magento\SomeModule\Controller\ControllerName\SomeClass', + '$this->getUrl("cms/*/index")', + [ + 'route_id' => 'cms', + 'controller_name' => 'controllername', + 'action_name' => 'index' + ] + ], + 'adminhtml wildcard controller route' => [ + 'Magento\Backend\Controller\Adminhtml\System\Store\DeleteStore', + '$this->getUrl("adminhtml/*/deleteStorePost")', + [ + 'route_id' => 'adminhtml', + 'controller_name' => 'system_store', + 'action_name' => 'deletestorepost' + ] + ], + 'index wildcard' => [ + 'Magento\Backend\Controller\System\Store\DeleteStore', + '$this->getUrl("routeid/controllername/*")', + [ + 'route_id' => 'routeid', + 'controller_name' => 'controllername', + 'action_name' => 'deletestore' + ] + ] + ]; + } + /** * @param string $class * @param string $content @@ -427,6 +529,10 @@ private function makeWebApiConfigReaderMock() '/V1/products/:sku/options' => ['GET' => ['service' => [ 'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface', 'method' => 'getList' + ] ] ], + '/V1/products' => ['GET' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface', + 'method' => 'getList' ] ] ] ] ]; From b7d5fa592063413e16251cc3743575d4baef5e5b Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 23 Oct 2020 12:10:55 +0300 Subject: [PATCH 098/195] fix confirm customer by token --- .../ConfirmCustomerByToken.php | 40 ++++++++++++------- .../Customer/Model/ResourceModel/Customer.php | 16 -------- .../ConfirmCustomerByTokenTest.php | 38 ++++++++++-------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php index e8e9ac9764c3b..1000575805018 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php @@ -7,7 +7,9 @@ namespace Magento\Customer\Model\ForgotPasswordToken; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Exception\LocalizedException; /** * Confirm customer by reset password token @@ -20,37 +22,47 @@ class ConfirmCustomerByToken private $getByToken; /** - * @var CustomerResource + * @var CustomerRepositoryInterface */ - private $customerResource; + private $customerRepository; /** - * ConfirmByToken constructor. - * * @param GetCustomerByToken $getByToken - * @param CustomerResource $customerResource + * @param CustomerRepositoryInterface $customerRepository */ - public function __construct( - GetCustomerByToken $getByToken, - CustomerResource $customerResource - ) { + public function __construct(GetCustomerByToken $getByToken, CustomerRepositoryInterface $customerRepository) + { $this->getByToken = $getByToken; - $this->customerResource = $customerResource; + $this->customerRepository = $customerRepository; } /** * Confirm customer account my rp_token * * @param string $resetPasswordToken - * * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function execute(string $resetPasswordToken): void { $customer = $this->getByToken->execute($resetPasswordToken); if ($customer->getConfirmation()) { - $this->customerResource->updateColumn($customer->getId(), 'confirmation', null); + $this->resetConfirmation($customer); } } + + /** + * Reset customer confirmation + * + * @param CustomerInterface $customer + * @return void + */ + private function resetConfirmation(CustomerInterface $customer): void + { + // skip unnecessary address and customer validation + $customer->setData('ignore_validation_flag', true); + $customer->setConfirmation(null); + + $this->customerRepository->save($customer); + } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index e0a79822ebeb8..1477287f79f4b 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -403,20 +403,4 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c } return $this; } - - /** - * @param int $customerId - * @param string $column - * @param string $value - */ - public function updateColumn($customerId, $column, $value) - { - $this->getConnection()->update( - $this->getTable('customer_entity'), - [$column => $value], - [$this->getEntityIdField() . ' = ?' => $customerId] - ); - - return $this; - } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php index 30aa70e89d2d0..4a6769e0653ad 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -7,11 +7,10 @@ namespace Magento\Customer\Test\Unit\Model\ForgotPasswordToken; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; -use Magento\Customer\Model\ResourceModel\Customer; use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken; +use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -33,22 +32,26 @@ class ConfirmCustomerByTokenTest extends TestCase private $customerMock; /** - * @var CustomerResource|MockObject + * @var CustomerRepositoryInterface|MockObject */ - private $customerResourceMock; + private $customerRepositoryMock; /** * @inheritDoc */ protected function setUp(): void { - $this->customerMock = $this->getMockForAbstractClass(CustomerInterface::class); - $this->customerResourceMock = $this->createMock(CustomerResource::class); + $this->customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->addMethods(['setData']) + ->getMockForAbstractClass(); + + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); $getCustomerByTokenMock = $this->createMock(GetCustomerByToken::class); $getCustomerByTokenMock->method('execute')->willReturn($this->customerMock); - $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerResourceMock); + $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerRepositoryMock); } /** @@ -58,17 +61,18 @@ protected function setUp(): void */ public function testExecuteWithConfirmation(): void { - $customerId = 777; - $this->customerMock->expects($this->once()) ->method('getConfirmation') ->willReturn('GWz2ik7Kts517MXAgrm4DzfcxKayGCm4'); $this->customerMock->expects($this->once()) - ->method('getId') - ->willReturn($customerId); - $this->customerResourceMock->expects($this->once()) - ->method('updateColumn') - ->with($customerId, 'confirmation', null); + ->method('setData') + ->with('ignore_validation_flag', true); + $this->customerMock->expects($this->once()) + ->method('setConfirmation') + ->with(null); + $this->customerRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->customerMock); $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); } @@ -83,8 +87,8 @@ public function testExecuteWithoutConfirmation(): void $this->customerMock->expects($this->once()) ->method('getConfirmation') ->willReturn(null); - $this->customerResourceMock->expects($this->never()) - ->method('updateColumn'); + $this->customerRepositoryMock->expects($this->never()) + ->method('save'); $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); } From 08ba757ff90f3515fdaa74495ef4b31937e64b1c Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 23 Oct 2020 13:23:45 +0300 Subject: [PATCH 099/195] MC-37884: [GraphQL] Cart Price Rule for the Whole Cart with coupon does not apply --- .../QuoteGraphQl/Model/Resolver/CartPrices.php | 17 ++++++++++++++++- .../Quote/Guest/ApplyCouponsToCartTest.php | 13 +++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php index 6a57a7662af09..5eb4d090a0f7b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -14,6 +14,7 @@ use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\TotalsCollector; +use Magento\SalesRule\Model\Spi\QuoteResetAppliedRulesInterface; /** * @inheritdoc @@ -25,13 +26,21 @@ class CartPrices implements ResolverInterface */ private $totalsCollector; + /** + * @var QuoteResetAppliedRulesInterface + */ + private $resetAppliedRules; + /** * @param TotalsCollector $totalsCollector + * @param QuoteResetAppliedRulesInterface $resetAppliedRules */ public function __construct( - TotalsCollector $totalsCollector + TotalsCollector $totalsCollector, + QuoteResetAppliedRulesInterface $resetAppliedRules ) { $this->totalsCollector = $totalsCollector; + $this->resetAppliedRules = $resetAppliedRules; } /** @@ -45,6 +54,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Quote $quote */ $quote = $value['model']; + /** + * To calculate a right discount value + * before calculate totals + * need to reset Cart Fixed Rules in the quote + */ + $this->resetAppliedRules->execute($quote); $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); $currency = $quote->getQuoteCurrencyCode(); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php index d33d0ee0569cd..a9dadccaa5373 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php @@ -21,6 +21,9 @@ class ApplyCouponsToCartTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; + /** + * @inheritdoc + */ protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); @@ -36,12 +39,17 @@ protected function setUp(): void public function testApplyCouponsToCart() { $couponCode = '2?ds5!2d'; + $expectedGrandTotal = 15.00; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId, $couponCode); $response = $this->graphQlMutation($query); self::assertArrayHasKey('applyCouponToCart', $response); self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + self::assertEquals( + $expectedGrandTotal, + $response['applyCouponToCart']['cart']['prices']['grand_total']['value'] + ); } /** @@ -146,6 +154,11 @@ private function getQuery(string $maskedQuoteId, string $couponCode): string applied_coupons { code } + prices { + grand_total { + value + } + } } } } From bee2147b02fe43df6e1333ef1ae5a8bf52676c04 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 23 Oct 2020 14:57:53 +0300 Subject: [PATCH 100/195] MC-37884: [GraphQL] Cart Price Rule for the Whole Cart with coupon does not apply --- .../QuoteGraphQl/Model/Resolver/CartPrices.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php index 5eb4d090a0f7b..66cc9ed11ed9f 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -14,7 +14,6 @@ use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\TotalsCollector; -use Magento\SalesRule\Model\Spi\QuoteResetAppliedRulesInterface; /** * @inheritdoc @@ -26,21 +25,13 @@ class CartPrices implements ResolverInterface */ private $totalsCollector; - /** - * @var QuoteResetAppliedRulesInterface - */ - private $resetAppliedRules; - /** * @param TotalsCollector $totalsCollector - * @param QuoteResetAppliedRulesInterface $resetAppliedRules */ public function __construct( - TotalsCollector $totalsCollector, - QuoteResetAppliedRulesInterface $resetAppliedRules + TotalsCollector $totalsCollector ) { $this->totalsCollector = $totalsCollector; - $this->resetAppliedRules = $resetAppliedRules; } /** @@ -59,7 +50,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * before calculate totals * need to reset Cart Fixed Rules in the quote */ - $this->resetAppliedRules->execute($quote); + $quote->setCartFixedRules([]); $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); $currency = $quote->getQuoteCurrencyCode(); From 5bd9efb6711346b646247c2eef3afd462b89e4ab Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 23 Oct 2020 15:12:09 +0300 Subject: [PATCH 101/195] MC-37663: Cannot invoice orders which contain bundle products comprised of physical and virtual products --- ...eOrderWithVirtualAndSimpleChildrenTest.xml | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml new file mode 100644 index 0000000000000..8cc26fd92b94c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -0,0 +1,98 @@ +<?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="StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product placing order"/> + <title value="Admin should be able to invoice order for the bundle product with virtual and simple products in options"/> + <description value="Place order for bundle product and create invoice"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38683"/> + <useCaseId value="MC-37663"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <!--Create bundle product with fixed price with simple and virtual products in options--> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem1"> + <field key="price">100.00</field> + </createData> + <createData entity="VirtualProduct" stepKey="createProductForBundleItem2"> + <field key="price">50.00</field> + </createData> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="createProductForBundleItem1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="createProductForBundleItem2"/> + </createData> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$$createFixedBundleProduct.id$$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <!--Perform reindex and flush cache--> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProductForBundleItem1" stepKey="deleteProductForBundleItem1"/> + <deleteData createDataKey="createProductForBundleItem2" stepKey="deleteProductForBundleItem2"/> + <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage"> + <argument name="product" value="$createFixedBundleProduct$"/> + </actionGroup> + <!-- Add bundle to cart --> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> + <argument name="productUrl" value="$$createFixedBundleProduct.name$$"/> + </actionGroup> + <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> + <!--Create Invoice for this Order--> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoiceActionGroup" stepKey="submitInvoice"/> + </test> +</tests> From 18b61947a375c86deb67e490e3a89c612ecbcdd3 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Fri, 23 Oct 2020 16:24:07 +0300 Subject: [PATCH 102/195] MC-37954: PLP sort by name is case-sensitive with ElasticSearch --- .../BatchDataMapper/ProductDataMapper.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 9fa001097df87..53472710671a4 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -90,6 +90,13 @@ class ProductDataMapper implements BatchDataMapperInterface */ private $filterableAttributeTypes; + /** + * @var string[] + */ + private $sortableCaseSensitiveAttributes = [ + 'name', + ]; + /** * @param Builder $builder * @param FieldMapperInterface $fieldMapper @@ -99,6 +106,7 @@ class ProductDataMapper implements BatchDataMapperInterface * @param array $excludedAttributes * @param array $sortableAttributesValuesToImplode * @param array $filterableAttributeTypes + * @param array $sortableCaseSensitiveAttributes */ public function __construct( Builder $builder, @@ -108,7 +116,8 @@ public function __construct( DataProvider $dataProvider, array $excludedAttributes = [], array $sortableAttributesValuesToImplode = [], - array $filterableAttributeTypes = [] + array $filterableAttributeTypes = [], + array $sortableCaseSensitiveAttributes = [] ) { $this->builder = $builder; $this->fieldMapper = $fieldMapper; @@ -122,6 +131,10 @@ public function __construct( $this->dataProvider = $dataProvider; $this->attributeOptionsCache = []; $this->filterableAttributeTypes = $filterableAttributeTypes; + $this->sortableCaseSensitiveAttributes = array_merge( + $this->sortableCaseSensitiveAttributes, + $sortableCaseSensitiveAttributes + ); } /** @@ -298,6 +311,12 @@ function (string $valueId) { $attributeValues = [$productId => implode(' ', $attributeValues)]; } + if (in_array($attribute->getAttributeCode(), $this->sortableCaseSensitiveAttributes)) { + foreach ($attributeValues as $key => $attributeValue) { + $attributeValues[$key] = strtolower($attributeValue); + } + } + return $attributeValues; } From a851e7bd14f4e59cc23a0b669d464dfac705f673 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz <lukasz.bajsarowicz@gmail.com> Date: Fri, 23 Oct 2020 15:34:30 +0200 Subject: [PATCH 103/195] Update app/code/Magento/Cms/Block/BlockByIdentifier.php Co-authored-by: Ihor Sviziev <ihor-sviziev@users.noreply.github.com> --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 464a002563323..eb8bf3d5fe352 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -85,7 +85,7 @@ protected function _toHtml(): string */ private function getIdentifier(): ?string { - return $this->getdata('identifier') ?: null; + return $this->getData('identifier') ?: null; } /** From ce38ff69fa46176c4af875d34de26d3d2b9aab94 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 23 Oct 2020 16:38:54 +0300 Subject: [PATCH 104/195] MC-37663: Cannot invoice orders which contain bundle products comprised of physical and virtual products --- ...eOrderWithVirtualAndSimpleChildrenTest.xml | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml index 8cc26fd92b94c..fe4faed29d144 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -20,10 +20,10 @@ <before> <createData entity="CustomerEntityOne" stepKey="createCustomer"/> <!--Create bundle product with fixed price with simple and virtual products in options--> - <createData entity="SimpleProduct2" stepKey="createProductForBundleItem1"> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">100.00</field> </createData> - <createData entity="VirtualProduct" stepKey="createProductForBundleItem2"> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> <field key="price">50.00</field> </createData> <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> @@ -36,34 +36,28 @@ <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> <requiredEntity createDataKey="createFixedBundleProduct"/> <requiredEntity createDataKey="createFirstBundleOption"/> - <requiredEntity createDataKey="createProductForBundleItem1"/> + <requiredEntity createDataKey="createSimpleProduct"/> </createData> <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> <requiredEntity createDataKey="createFixedBundleProduct"/> <requiredEntity createDataKey="createSecondBundleOption"/> - <requiredEntity createDataKey="createProductForBundleItem2"/> + <requiredEntity createDataKey="createVirtualProduct"/> </createData> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> - <argument name="productId" value="$$createFixedBundleProduct.id$$"/> + <argument name="productId" value="$createFixedBundleProduct.id$"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Perform reindex and flush cache--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> - </actionGroup> + <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> </before> <after> - <deleteData createDataKey="createProductForBundleItem1" stepKey="deleteProductForBundleItem1"/> - <deleteData createDataKey="createProductForBundleItem2" stepKey="deleteProductForBundleItem2"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProductForBundleItem"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProductForBundleItem"/> <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Login customer on storefront--> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> @@ -73,9 +67,9 @@ <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage"> <argument name="product" value="$createFixedBundleProduct$"/> </actionGroup> - <!-- Add bundle to cart --> + <!--Add bundle to cart--> <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> - <argument name="productUrl" value="$$createFixedBundleProduct.name$$"/> + <argument name="productUrl" value="$createFixedBundleProduct.name$"/> </actionGroup> <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> <argument name="quantity" value="1"/> From 9e2462c761b1b7b6cfabddba3466e7560baaa7e2 Mon Sep 17 00:00:00 2001 From: rrego6 <rrego@adobe.com> Date: Fri, 23 Oct 2020 11:12:59 -0400 Subject: [PATCH 105/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Fixed static tests --- .../Magento/TestFramework/Dependency/PhpRuleTest.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index ec18a58245c1b..0d12a62884a39 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -228,7 +228,6 @@ public function getDependencyInfoDataProvider() ]; } - /** * @param string $class * @param string $content @@ -313,7 +312,6 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() ]; } - /** * @param string $template * @param string $content @@ -331,11 +329,14 @@ public function testGetDependencyInfoCaseTemplateGetUrl( $this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $template, $content)); } + /** + * @return array[] + */ public function getDependencyInfoDataCaseGetTemplateUrlDataProvider() { return [ 'getUrl from ignore template' => [ 'app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar/script.phtml', - '$getUrl("adminhtml/*/ajaxBlock"', + '$getUrl("adminhtml/*/ajaxBlock")', []]]; } @@ -373,6 +374,9 @@ public function testProcessWildcardUrl( $phpRule->getDependencyInfo($module, 'php', $file, $content); } + /** + * @return array[] + */ public function processWildcardUrlDataProvider() { return [ From 0d002e3fb7530c98abc66349329ed6e3cae03cc9 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Fri, 23 Oct 2020 19:11:50 +0300 Subject: [PATCH 106/195] Unfinished test. Not sure how to check element's css visibility. --- .../Test/Mftf/Section/AdminHeaderSection.xml | 1 + .../Test/Mftf/Test/AdminSearchHotkeyTest.xml | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index 186bb183d68d6..f1e2ad911dfbc 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -11,6 +11,7 @@ <section name="AdminHeaderSection"> <element name="pageTitle" type="text" selector=".page-header h1.page-title"/> <element name="adminUserAccountText" type="text" selector=".page-header .admin-user-account-text" /> + <element name="globalSearchInput" type="text" selector="#search-global" /> <!-- Legacy heading section. Mostly used for admin 404 and 403 pages --> <element name="pageHeading" type="text" selector=".page-content .page-heading"/> <!-- Used for page not found error --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml new file mode 100644 index 0000000000000..ae700332b948d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -0,0 +1,35 @@ +<?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="AdminSearchHotkeyTest"> + <annotations> + <features value="Backend"/> + <stories value="Search form hotkey in backend"/> + <title value="Admin should be able focus on the search field with a hotkey"/> + <description value="Admin should be able focus on the search field with a hotkey - forwardslash"/> + <severity value="MINOR"/> + <group value="backend"/> + <group value="search"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <pressKey selector="body" parameterArray="[/]" stepKey="pressForwardslashKey"/> + <waitForElementVisible selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="waitForGlobalSearchInput"/> + <seeElement selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeGlobalSearchInput"/> + <seeInField userInput="" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeEmptyGlobalSearchInput"/> + <pressKey selector="{{AdminHeaderSection.globalSearchInput}}" parameterArray="[/]" stepKey="pressForwardslashKeyAgain"/> + <seeInField userInput="/" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeForwardSlashInGlobalSearchInput"/> + </test> +</tests> From 63a5bd20dadcffea89703bc8605b4974bb0a0850 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Fri, 23 Oct 2020 16:19:47 -0500 Subject: [PATCH 107/195] [AWS S3] Cover Downloadable and CMS functionality by functional tests (#6240) --- .../AwsS3AdminAddImageToWYSIWYGBlockTest.xml | 80 ++++++++ .../AwsS3AdminAddImageToWYSIWYGCMSTest.xml | 75 +++++++ ...nCreateDownloadableProductWithLinkTest.xml | 186 ++++++++++++++++++ .../StorefrontDownloadableLinkSection.xml | 15 ++ 4 files changed, 356 insertions(+) create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml create mode 100644 app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml new file mode 100644 index 0000000000000..54b7795a84cd3 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml @@ -0,0 +1,80 @@ +<?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="AwsS3AdminAddImageToWYSIWYGBlockTest"> + <annotations> + <features value="Cms"/> + <stories value="MC-37460: Support by Magento CMS"/> + <group value="Cms"/> + <title value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <description value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38302"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> + <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> + <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + </before> + <actionGroup ref="AssignBlockToCMSPage" stepKey="assignBlockToCMSPage"> + <argument name="Block" value="$$createPreReqBlock$$"/> + <argument name="CmsPage" value="$$createCMSPage$$"/> + </actionGroup> + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> + </actionGroup> + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="All Store View" stepKey="selectAllStoreView" /> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> + <argument name="Image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> + <argument name="Image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> + <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="clickSaveBlock"/> + <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="waitForPageLoad11" /> + <!--see image on Storefront--> + <seeElement selector="{{StorefrontBlockSection.mediaDescription}}" stepKey="assertMediaDescription"/> + <seeElementInDOM selector="{{StorefrontBlockSection.imageSource(ImageUpload.fileName)}}" stepKey="assertMediaSource"/> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnEditPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> + <waitForPageLoad stepKey="waitForGridReload"/> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml new file mode 100644 index 0000000000000..d361fb60f31c7 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml @@ -0,0 +1,75 @@ +<?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="AwsS3AdminAddImageToWYSIWYGCMSTest"> + <annotations> + <features value="Cms"/> + <stories value="MC-37460: Support by Magento CMS"/> + <group value="Cms"/> + <title value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <description value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38295"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> + <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> + <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> + <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="$$createCMSPage.identifier$$" stepKey="fillFieldUrlKey"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="wait4"/> + <seeElement selector="{{StorefrontCMSPageSection.mediaDescription}}" stepKey="assertMediaDescription"/> + <seeElementInDOM selector="{{StorefrontCMSPageSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml new file mode 100644 index 0000000000000..449f00281c425 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml @@ -0,0 +1,186 @@ +<?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="AwsS3AdminCreateDownloadableProductWithLinkTest"> + <annotations> + <features value="Catalog"/> + <stories value="Support remote file storage by downloadable products"/> + <title value="Create, view and check out downloadable product with remote filesystem configured. "/> + <description value="Admin should be able to create downloadable product with remote filesystem enabled"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38036"/> + <testCaseId value="MC-38037"/> + <testCaseId value="MC-38039"/> + <group value="Downloadable"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Create downloadable product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Add downloadable links --> + <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection"/> + <checkOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> + <fillField userInput="{{downloadableData.link_title}}" selector="{{AdminProductDownloadableSection.linksTitleInput}}" stepKey="fillDownloadableLinkTitle"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkLinksPurchasedSeparately"/> + <fillField userInput="{{downloadableData.sample_title}}" selector="{{AdminProductDownloadableSection.samplesTitleInput}}" stepKey="fillDownloadableSampleTitle"/> + <actionGroup ref="AddDownloadableProductLinkWithMaxDownloadsActionGroup" stepKey="addDownloadableLinkWithMaxDownloads"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="AddDownloadableProductLinkActionGroup" stepKey="addDownloadableLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + <!-- Add downloadable sample--> + <actionGroup ref="AddDownloadableSampleFileActionGroup" stepKey="addDownloadableProductSample"/> + + <!-- Save product --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> + + <!-- Login to frontend --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Assert product in storefront category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckProductPriceInCategoryActionGroup" stepKey="StorefrontCheckCategorySimpleProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Assert product in storefront product page --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageActionGroup" stepKey="AssertProductInStorefrontProductPage"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Assert product price in storefront product page --> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{DownloadableProduct.price}}" stepKey="assertProductPrice"/> + + <!-- Assert link sample urls are accessible --> + <!-- Click on the link sample --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLinkSampleWithMaxDownloads"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLinkWithMaxDownloads.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="clickDownloadableLinkSampleWithMaxDownloads"/> + <waitForPageLoad stepKey="waitForLinkSampleWithMaxDownloadsPage"/> + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleWithMaxDownloadsTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkWithMaxDownloadsId"/> + <!-- Check is svg --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedSvg('Logo')}}" stepKey="assertDownloadableLinkWithMaxDownloadsIsSvg"/> + <closeTab stepKey="closeLinkSampleWithMaxDownloadsTab"/> + <!-- Click on the link sample --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLinkSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink.title)}}" stepKey="clickDownloadableLinkSample"/> + <waitForPageLoad stepKey="waitForLinkSamplePage"/> + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkSampleId"/> + <!-- Check is image --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedImage}}" stepKey="assertDownloadableLinkSampleIsImage"/> + <closeTab stepKey="closeLinkSampleTab"/> + + <!-- Assert sample file is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableSampleLabel(downloadableSampleFile.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableSampleLabel(downloadableSampleFile.title)}}" stepKey="clickDownloadableSample"/> + <waitForPageLoad stepKey="waitForSamplePage"/> + <!-- Grab Sample id --> + <switchToNextTab stepKey="switchToSampleTab"/> + <grabFromCurrentUrl regex="~/sample_id/(\d+)/~" stepKey="grabDownloadableSampleId"/> + <!-- Check is image --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedImage}}" stepKey="assertDownloadableSampleIsImage"/> + <closeTab stepKey="closeSampleTab"/> + + <!-- Select product link in storefront product page--> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinks"/> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="selectProductLink"/> + + <!-- Add product with selected link to the cart --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="DownloadableProduct"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Assert product price in cart --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$52.99" stepKey="assertProductPriceInCart"/> + + <!-- Perform checkout --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Open created order --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchOrder"> + <argument name="keyword" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + + <!-- Open Create invoice --> + <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createCreditMemo"/> + + <!-- Check downloadable product link on frontend --> + <actionGroup ref="StorefrontAssertDownloadableProductIsPresentInCustomerAccount" stepKey="seeStorefrontMyAccountDownloadableProductsLink"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + <click selector="{{StorefrontCustomerDownloadableProductsSection.downloadableLink}}" stepKey="clickDownloadLink" /> + <waitForPageLoad stepKey="waitForDownloadedLinkPage"/> + <!-- Grab downloadable URL --> + <switchToNextTab stepKey="switchToDownloadedLinkTab"/> + <grabFromCurrentUrl regex="~/link/id/(.+)/~" stepKey="grabDownloadLinkUrl"/> + <!-- Check is svg --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedSvg('Logo')}}" stepKey="assertDownloadedLinkIsSvg"/> + <closeTab stepKey="closeDownloadedLinkTab"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml new file mode 100644 index 0000000000000..6364600faee30 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontDownloadableLinkSection"> + <element name="downloadedImage" type="text" selector="//img[contains(@style, '-webkit-user-select')]"/> + <element name="downloadedSvg" type="text" selector="//*[@id='{{id}}']" parameterized="true"/> + </section> +</sections> From bb7d50bcc838dcf66fc325e7e9ecbfdd8f1637aa Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Sat, 24 Oct 2020 17:13:17 +0300 Subject: [PATCH 108/195] add Backward Compatibility --- .../CaptchaWithDisabledGuestCheckoutTest.xml | 3 +++ .../StorefrontCatalogNavigationMenuUIDesktopTest.xml | 9 +++++++++ ...ateFieldForUKCustomerRemainOptionAfterRefreshTest.xml | 1 + ...igsChangesIsNotAffectedStartedCheckoutProcessTest.xml | 1 + ...refrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml | 2 ++ ...rsistentDataForGuestCustomerWithPhysicalQuoteTest.xml | 3 +++ ...rontUpdatePriceInShoppingCartAfterProductSaveTest.xml | 1 + .../Test/StorefrontVisibilityOfDuplicateProductTest.xml | 4 ++++ .../Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml | 2 ++ ...dminFrontendAreaSessionMustNotAffectAdminAreaTest.xml | 2 ++ .../CheckShoppingCartBehaviorAfterSessionExpiredTest.xml | 1 + .../Test/StorefrontGuestCheckoutDisabledProductTest.xml | 3 +++ .../AdminCreateCartPriceRuleForGeneratedCouponTest.xml | 1 + .../Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml | 1 + .../Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml | 1 + .../Test/StorefrontInlineTranslationOnCheckoutTest.xml | 3 +++ 16 files changed, 38 insertions(+) diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml index 39a774369c331..66183cb31aebc 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -46,7 +46,10 @@ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad2" /> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart2"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout2"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index 07c67f6f290f1..fb4bd4d1dcb74 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -40,7 +40,10 @@ <!-- Assert single row - no hover state --> <createData entity="ApiCategoryA" stepKey="createFirstCategoryBlank"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForBlankSingleRowAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryBlank.name$$)}}" stepKey="hoverFirstCategoryBlank"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}" stepKey="assertNoHoverState"/> @@ -87,6 +90,8 @@ <!-- Several rows. Hover on category without children --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForBlankSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithoutChildrenBlank.name$$)}}" stepKey="hoverCategoryWithoutChildren"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createCategoryWithoutChildrenBlank.name$$, 'level0')}}" stepKey="dontSeeChildrenInCategory"/> @@ -166,6 +171,8 @@ <!-- Single row. No hover state --> <actionGroup ref="ReloadPageActionGroup" stepKey="reload"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLumaSingleRowAppear"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFirstCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFirstCategory"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createSecondCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInSecondCategory"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createThirdCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateThirdCategory"/> @@ -201,6 +208,8 @@ <!-- Several rows. Hover on Category without children --> <actionGroup ref="ReloadPageActionGroup" stepKey="refresh"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLumaSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFifthCategoryLuma.name$$)}}" stepKey="hoverOnCategoryWithoutChildren"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFifthCategoryLuma.name$$, 'level0')}}" stepKey="dontSeeSubcategoriesInCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index de4e64e3c5938..f578a9c02caca 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -43,6 +43,7 @@ <selectOption stepKey="selectCounty" selector="{{CheckoutShippingSection.country}}" userInput="{{UK_Address.country_id}}"/> <waitForPageLoad stepKey="waitFormToReload"/> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1" /> <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index 3128387e4155b..df229c4b6ed78 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -91,6 +91,7 @@ <!-- Back to the Checkout and refresh the page --> <switchToPreviousTab stepKey="switchToPreviousTab"/> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitPageReload"/> <!-- Payment step is opened after refreshing --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSection"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 67cf37f75c979..033898bb90557 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -173,6 +173,8 @@ <argument name="address" value="US_Address_NY_Default_Shipping"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadThePage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageToReload"/> + <waitForText selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.60" time="90" stepKey="waitForTaxAmount"/> <!--Select Free Shipping and proceed to checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index 6ac85e77766e1..a3c093d005371 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -50,6 +50,7 @@ <see selector="{{CheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> <!-- 5. Refresh browser page (F5) --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad"/> <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> <argument name="carrierCode" value="flatrate"/> @@ -71,6 +72,8 @@ <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> <!-- 10. Refresh browser page(F5) --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadCheckoutPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> <argument name="shippingMethod" value="Free Shipping"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index 299d33244f1fb..f014a7a5bd1ee 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -63,6 +63,7 @@ <!--Check price--> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index a066d5077f713..79705e679fb78 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -63,6 +63,8 @@ <argument name="scope" value="Global"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForProductPageReload"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> <waitForPageLoad stepKey="waitForFilters"/> <actionGroup ref="CreateOptionsForAttributeActionGroup" stepKey="createOptions"> @@ -142,6 +144,8 @@ <argument name="scope" value="Global"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadDuplicatedProductPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForDuplicatedProductReload"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="createConfigurationsDuplicatedProduct"/> <waitForElementVisible selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="waitForCreateConfigurationsPageLoad"/> <click selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 710e4bba29e05..7442a32d58b2d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -41,6 +41,8 @@ <argument name="indices" value=""/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLoad2"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index 9eb0558bdfd2e..e026bee87dcd4 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -77,7 +77,9 @@ <!-- 5. Open admin tab with page with products. Reload this page twice. --> <switchToPreviousTab stepKey="switchToPreviousTab"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadAdminCatalogPageFirst"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReloadFirst"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadAdminCatalogPageSecond"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReloadSecond"/> <seeInTitle userInput="Products / Inventory / Catalog / Magento Admin" stepKey="seeAdminProductsPageTitle"/> <see userInput="Products" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeAdminProductsPageHeader"/> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml index 533a06986b70a..5b023e12bc55d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml @@ -56,6 +56,7 @@ <!--Reset cookies and refresh the page--> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad"/> <!--Check product exists in cart--> <see userInput="$$createProduct.name$$" stepKey="ProductExistsInCart"/> </test> diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 92a1c1facd6a6..ee5f2fccfe203 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -125,6 +125,7 @@ <!-- Check cart --> <wait time="60" stepKey="waitForCartToBeUpdated"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickMiniCart"/> <dontSeeElement selector="{{StorefrontMinicartSection.quantity}}" stepKey="dontSeeCartItem"/> <!-- Add simple product to shopping cart --> @@ -151,6 +152,8 @@ <!--Check cart--> <wait time="60" stepKey="waitForCartToBeUpdated2"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage2"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickMiniCart2"/> <dontSeeElement selector="{{StorefrontMinicartSection.quantity}}" stepKey="dontSeeCartItem2"/> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 79672b5bdd559..b3d81cea7f97f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -60,6 +60,7 @@ <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1"/> <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection2"/> <!-- Assert coupon codes grid header is correct --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 96b3990dfd063..74542be376c45 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -64,6 +64,7 @@ <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1"/> <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" stepKey="clickManageCouponCodes2"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 8ad9578e9184a..85481f6fd4d5f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -154,6 +154,7 @@ <!-- Verify swatch tooltips are not visible --> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageReload"/> <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverDisabledSwatch"/> <wait time="1" stepKey="waitForTooltip2"/> <dontSeeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipNotVisible"/> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index cfee0785ac1d1..4eff032ce160e 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -121,6 +121,7 @@ <!-- 3. Go to storefront and click on cart button on the top --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReload"/> <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> @@ -490,6 +491,8 @@ <!-- Reload page after full clear --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPageAfterFullClean"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoadAfterFullClean"/> + <!-- Add product to cart and go through Checkout process like you did in steps ##3-6 and check translation you maid. --> <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductPage1"> From a93afdef222b6fbadc68c0a3b452346d3bdf3016 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Sat, 24 Oct 2020 22:30:16 +0300 Subject: [PATCH 109/195] Fixed invalid test. Now it checks if parent element has active class. --- .../Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml | 1 + .../Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index f1e2ad911dfbc..4ebb3316a0245 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -12,6 +12,7 @@ <element name="pageTitle" type="text" selector=".page-header h1.page-title"/> <element name="adminUserAccountText" type="text" selector=".page-header .admin-user-account-text" /> <element name="globalSearchInput" type="text" selector="#search-global" /> + <element name="globalSearchInputVisible" type="text" selector=".search-global-field._active #search-global" /> <!-- Legacy heading section. Mostly used for admin 404 and 403 pages --> <element name="pageHeading" type="text" selector=".page-content .page-heading"/> <!-- Used for page not found error --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml index ae700332b948d..89e8668fa3c23 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -26,8 +26,7 @@ </after> <pressKey selector="body" parameterArray="[/]" stepKey="pressForwardslashKey"/> - <waitForElementVisible selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="waitForGlobalSearchInput"/> - <seeElement selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeGlobalSearchInput"/> + <seeElement selector="{{AdminHeaderSection.globalSearchInputVisible}}" stepKey="seeActiveGlobalSearchInput"/> <seeInField userInput="" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeEmptyGlobalSearchInput"/> <pressKey selector="{{AdminHeaderSection.globalSearchInput}}" parameterArray="[/]" stepKey="pressForwardslashKeyAgain"/> <seeInField userInput="/" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeForwardSlashInGlobalSearchInput"/> From 3aa86ee47ac73aa12c5771bf8b4adaa278d15628 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 26 Oct 2020 11:01:06 +0200 Subject: [PATCH 110/195] MC-37954: PLP sort by name is case-sensitive with ElasticSearch --- .../Model/Adapter/BatchDataMapper/ProductDataMapper.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 53472710671a4..0edc63b10f9ab 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -272,6 +272,9 @@ private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool * @param array $attributeValues * @param int $storeId * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ private function prepareAttributeValues( int $productId, From 76b9492952301f7f4a7b888bac9cdc16d2568d9c Mon Sep 17 00:00:00 2001 From: engcom-Kilo <mikola.malevanec@transoftgroup.com> Date: Mon, 19 Oct 2020 17:48:24 +0300 Subject: [PATCH 111/195] MC-38509: Submitting invalid create account form leaves the submit button disabled. --- ...frontCreateCustomerWithInvalidDataTest.xml | 39 +++++++++++++++++++ .../frontend/web/js/block-submit-on-send.js | 6 +++ 2 files changed, 45 insertions(+) create mode 100644 app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml new file mode 100644 index 0000000000000..ef610831a721d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml @@ -0,0 +1,39 @@ +<?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="StorefrontCreateCustomerWithInvalidDataTest"> + <annotations> + <stories value="Create a Customer via the Storefront"/> + <features value="Customer"/> + <title value="Register customer on storefront after customer form validation failed."/> + <description value="Customer should be able to re-submit register form after correcting invalid form data on storefront."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38532"/> + <useCaseId value="MC-38509"/> + <group value="customer"/> + </annotations> + + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> + <!--Try to submit register form with wrong password.--> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountFormWithWrongData"> + <argument name="customer" value="Simple_Customer_With_Password_Length_Is_Below_Eight_Characters"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="tryToSubmitFormWithWrongPassword"/> + <actionGroup ref="AssertMessageCustomerCreateAccountPasswordComplexityActionGroup" stepKey="seeTheErrorPasswordLength"> + <argument name="message" value="Minimum length of this field must be equal or greater than 8 symbols. Leading and trailing spaces will be ignored."/> + </actionGroup> + <!--Re-submit customer register form with correct data.--> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountFormWithCorrectData"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js index b941ec7a254d8..75f4ee6097685 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js +++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js @@ -14,9 +14,15 @@ define([ dataForm.submit(function () { $(this).find(':submit').attr('disabled', 'disabled'); + + if (this.isValid === false) { + $(this).find(':submit').prop('disabled', false); + } + this.isValid = true; }); dataForm.bind('invalid-form.validate', function () { $(this).find(':submit').prop('disabled', false); + this.isValid = false; }); }; }); From c15c7c74d80711be59fcd75baf6c0f7da7419507 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 26 Oct 2020 13:13:15 +0200 Subject: [PATCH 112/195] MC-37954: PLP sort by name is case-sensitive with ElasticSearch --- .../Model/Indexer/ReindexAllTest.php | 39 ++++++ .../Elasticsearch/_files/case_sensitive.php | 126 ++++++++++++++++++ .../_files/case_sensitive_rollback.php | 35 +++++ 3 files changed, 200 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php create mode 100644 dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php index 9679b4f232ee2..6df4d8fbb2d92 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php @@ -130,6 +130,45 @@ public function testSort() $this->assertEquals($productSimpleId, $firstInSearchResults); } + /** + * Test sorting of products with lower and upper case names after full reindex + * + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @magentoDataFixture Magento/Elasticsearch/_files/case_sensitive.php + */ + public function testSortCaseSensitive(): void + { + $productFirst = $this->productRepository->get('fulltext-1'); + $productSecond = $this->productRepository->get('fulltext-2'); + $productThird = $this->productRepository->get('fulltext-3'); + $productFourth = $this->productRepository->get('fulltext-4'); + $productFifth = $this->productRepository->get('fulltext-5'); + $correctSortedIds = [ + $productFirst->getId(), + $productFourth->getId(), + $productSecond->getId(), + $productFifth->getId(), + $productThird->getId(), + ]; + $this->reindexAll(); + $result = $this->sortByName(); + $firstInSearchResults = (int) $result[0]['_id']; + $secondInSearchResults = (int) $result[1]['_id']; + $thirdInSearchResults = (int) $result[2]['_id']; + $fourthInSearchResults = (int) $result[3]['_id']; + $fifthInSearchResults = (int) $result[4]['_id']; + $actualSortedIds = [ + $firstInSearchResults, + $secondInSearchResults, + $thirdInSearchResults, + $fourthInSearchResults, + $fifthInSearchResults + ]; + $this->assertCount(5, $result); + $this->assertEquals($correctSortedIds, $actualSortedIds); + } + /** * Test search of specific product after full reindex * diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php new file mode 100644 index 0000000000000..1b664f958dd46 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute.php'); + +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = Bootstrap::getObjectManager(); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->get('fulltext-1'); +} catch (NoSuchEntityException $e) { + /** @var $productFirst Product */ + $productFirst = $objectManager->create(Product::class); + $productFirst->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('A') + ->setSku('fulltext-1') + ->setPrice(10) + ->setMetaTitle('first meta title') + ->setMetaKeyword('first meta keyword') + ->setMetaDescription('first meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-2'); +} catch (NoSuchEntityException $e) { + /** @var $productSecond Product */ + $productSecond = $objectManager->create(Product::class); + $productSecond->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('B') + ->setSku('fulltext-2') + ->setPrice(20) + ->setMetaTitle('second meta title') + ->setMetaKeyword('second meta keyword') + ->setMetaDescription('second meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-3'); +} catch (NoSuchEntityException $e) { + /** @var $productThird Product */ + $productThird = $objectManager->create(Product::class); + $productThird->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('C') + ->setSku('fulltext-3') + ->setPrice(20) + ->setMetaTitle('third meta title') + ->setMetaKeyword('third meta keyword') + ->setMetaDescription('third meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-4'); +} catch (NoSuchEntityException $e) { + /** @var $productFourth Product */ + $productFourth = $objectManager->create(Product::class); + $productFourth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('a') + ->setSku('fulltext-4') + ->setPrice(20) + ->setMetaTitle('fourth meta title') + ->setMetaKeyword('fourth meta keyword') + ->setMetaDescription('fourth meta description') + ->setUrlKey('aa') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} + +try { + $productRepository->get('fulltext-5'); +} catch (NoSuchEntityException $e) { + /** @var $productFifth Product */ + $productFifth = $objectManager->create(Product::class); + $productFifth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('b') + ->setSku('fulltext-5') + ->setPrice(20) + ->setMetaTitle('fifth meta title') + ->setMetaKeyword('fifth meta keyword') + ->setMetaDescription('fifth meta description') + ->setUrlKey('bb') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php new file mode 100644 index 0000000000000..a97faa29a1588 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute_rollback.php'); + +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +$collection->addAttributeToSelect('id')->load(); +if ($collection->count() > 0) { + $collection->delete(); +} + +/** @var \Magento\Store\Model\Store $store */ +$store = $objectManager->create(\Magento\Store\Model\Store::class); +$storeCode = 'secondary'; +$store->load($storeCode); +if ($store->getId()) { + $store->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 50d421dc84511efa196e3e07267ff8cc0ae6cd6f Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Mon, 26 Oct 2020 13:21:55 +0200 Subject: [PATCH 113/195] Use namespaced event listener, to make it removable --- app/design/adminhtml/Magento/backend/web/js/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index e2b8d8cfc884d..ac49462803f77 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -347,7 +347,7 @@ define('globalSearch', [ self.field.addClass(self.options.fieldActiveClass); }); - $(document).keydown(function (event) { + $(document).on('keydown.activateGlobalSearchForm', function (event) { var inputs = [ 'input', 'select', From 3a19dcad46475e3aaf7e82c48f1e859b1e2cd4a4 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Mon, 26 Oct 2020 13:24:28 +0200 Subject: [PATCH 114/195] Don't use whole jquery/ui when widget is needed only --- app/design/adminhtml/Magento/backend/web/js/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index ac49462803f77..069970deae681 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -313,7 +313,7 @@ define('globalNavigation', [ define('globalSearch', [ 'jquery', 'Magento_Ui/js/lib/key-codes', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, keyCodes) { 'use strict'; From 792875173830d061040e65cc41b66e9fb66826cc Mon Sep 17 00:00:00 2001 From: Dmytro Poperechnyy <dpoperechnyy@magento.com> Date: Mon, 26 Oct 2020 23:37:01 +0200 Subject: [PATCH 115/195] [AWS S3] MC-37601: Support by Magento ImportExport (#6231) --- app/code/Magento/AwsS3/Driver/AwsS3.php | 42 +++++-- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 19 ++- .../Model/Import/Product.php | 6 +- .../Model/Import/Uploader.php | 34 +++++- .../Test/Unit/Model/Import/ProductTest.php | 12 ++ .../Test/Unit/Model/Import/UploaderTest.php | 28 ++++- .../Helper/Uploader.php | 10 +- .../Adminhtml/Export/File/Download.php | 11 +- .../ImportExport/Model/Export/Consumer.php | 4 +- .../DataProvider/ExportFileDataProvider.php | 28 +++-- .../Magento/ImportExport/etc/adminhtml/di.xml | 2 + app/code/Magento/RemoteStorage/Filesystem.php | 15 +++ .../Magento/RemoteStorage/Plugin/Image.php | 85 ++++++++++--- .../Test/Unit/Plugin/ImageTest.php | 36 ++++-- app/code/Magento/RemoteStorage/composer.json | 5 +- app/code/Magento/RemoteStorage/etc/di.xml | 32 +++++ .../App/Filesystem/CreatePdfFileTest.php | 1 - .../Adminhtml/Export/File/DownloadTest.php | 2 +- .../App/Filesystem/DirectoryList.php | 8 +- .../Framework/Filesystem/Directory/Write.php | 6 +- lib/internal/Magento/Framework/Image.php | 9 +- .../Magento/Framework/Image/Adapter/Gd2.php | 112 +++++++++++------- .../Framework/Image/Adapter/ImageMagick.php | 7 ++ .../Test/Unit/Adapter/ImageMagickTest.php | 4 +- .../Unit/Adapter/_files/global_php_mock.php | 10 ++ 25 files changed, 405 insertions(+), 123 deletions(-) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index b7dee36488bb9..169a93580038c 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Phrase; +use Psr\Log\LoggerInterface; /** * Driver for AWS S3 IO operations. @@ -35,23 +36,35 @@ class AwsS3 implements DriverInterface */ private $streams = []; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param AwsS3Adapter $adapter + * @param LoggerInterface $logger */ - public function __construct(AwsS3Adapter $adapter) - { + public function __construct( + AwsS3Adapter $adapter, + LoggerInterface $logger + ) { $this->adapter = $adapter; + $this->logger = $logger; } /** * Destroy opened streams. - * - * @throws FileSystemException */ public function __destruct() { - foreach ($this->streams as $stream) { - $this->fileClose($stream); + try { + foreach ($this->streams as $stream) { + $this->fileClose($stream); + } + } catch (\Exception $e) { + // log exception as throwing an exception from a destructor causes a fatal error + $this->logger->critical($e); } } @@ -521,12 +534,17 @@ public function fileRead($resource, $length): string */ public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure = '"', $escape = '\\') { - //phpcs:disable - $metadata = stream_get_meta_data($resource); - //phpcs:enable - $file = $this->adapter->read($metadata['uri'])['contents']; - - return str_getcsv($file, $delimiter, $enclosure, $escape); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = fgetcsv($resource, $length, $delimiter, $enclosure, $escape); + if ($result === null) { + throw new FileSystemException( + new Phrase( + 'The "%1" CSV handle is incorrect. Verify the handle and try again.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index e3e3e4208484d..173143b709519 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\FileSystemException; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @see AwsS3 @@ -36,6 +37,11 @@ class AwsS3Test extends TestCase */ private $clientMock; + /** + * @var LoggerInterface + */ + private $logger; + /** * @inheritDoc */ @@ -43,6 +49,7 @@ protected function setUp(): void { $this->adapterMock = $this->createMock(AwsS3Adapter::class); $this->clientMock = $this->getMockForAbstractClass(S3ClientInterface::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->adapterMock->method('applyPathPrefix') ->willReturnArgument(0); @@ -59,7 +66,7 @@ protected function setUp(): void return self::URL . $path; }); - $this->driver = new AwsS3($this->adapterMock); + $this->driver = new AwsS3($this->adapterMock, $this->logger); } /** @@ -149,6 +156,16 @@ public function getAbsolutePathDataProvider(): array '', self::URL . 'media/catalog/test.png', self::URL . 'media/catalog/test.png' + ], + [ + self::URL, + 'var/import/images', + self::URL . 'var/import/images' + ], + [ + self::URL . 'var/import/images/product_images/', + self::URL . 'var/import/images/product_images/1.png', + self::URL . 'var/import/images/product_images/1.png' ] ]; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index f59bc338ced69..428961aa6ddf6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -9,7 +9,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Model\ResourceModel\Product\Link; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor; use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; @@ -2209,6 +2208,11 @@ protected function _getUploader() $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + // make media folder a primary folder for media in external storages + if (!is_a($this->_mediaDirectory->getDriver(), File::class)) { + $dirAddon = DirectoryList::MEDIA; + } + $tmpPath = $this->getImportDir(); if (!$fileUploader->setTmpDir($tmpPath)) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 5b90ced62b0eb..d2a0019349ef2 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverPool; /** @@ -116,6 +117,11 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ private $maxFilenameLength = 255; + /** + * @var TargetDirectory + */ + private $targetDirectory; + /** * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb * @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage @@ -125,6 +131,7 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader * @param Filesystem\File\ReadFactory $readFactory * @param string|null $filePath * @param \Magento\Framework\Math\Random|null $random + * @param TargetDirectory|null $targetDirectory * @throws \Magento\Framework\Exception\FileSystemException * @throws \Magento\Framework\Exception\LocalizedException */ @@ -136,7 +143,8 @@ public function __construct( Filesystem $filesystem, Filesystem\File\ReadFactory $readFactory, $filePath = null, - \Magento\Framework\Math\Random $random = null + \Magento\Framework\Math\Random $random = null, + TargetDirectory $targetDirectory = null ) { $this->_imageFactory = $imageFactory; $this->_coreFileStorageDb = $coreFileStorageDb; @@ -149,6 +157,7 @@ public function __construct( $this->_setUploadFile($filePath); } $this->random = $random ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class); + $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); } /** @@ -188,7 +197,8 @@ public function move($fileName, $renameFileOff = false) } $this->_setUploadFile($tmpFilePath); - $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); + $rootDirectory = $this->getTargetDirectory()->getDirectoryRead(DirectoryList::ROOT); + $destDir = $rootDirectory->getAbsolutePath($this->getDestDir()); $result = $this->save($destDir); unset($result['path']); $result['name'] = self::getCorrectFileName($result['name']); @@ -243,6 +253,20 @@ private function downloadFileFromUrl($url, $driver) return $tmpFilePath; } + /** + * Retrieves target directory. + * + * @return TargetDirectory + */ + private function getTargetDirectory(): TargetDirectory + { + if (!isset($this->targetDirectory)) { + $this->targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + } + + return $this->targetDirectory; + } + /** * Prepare information about the file for moving * @@ -381,7 +405,8 @@ public function getDestDir() */ public function setDestDir($path) { - if (is_string($path) && $this->_directory->isWritable($path)) { + $directoryRoot = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + if (is_string($path) && $directoryRoot->isWritable($path)) { $this->_destDir = $path; return true; } @@ -404,7 +429,8 @@ protected function _moveFile($tmpPath, $destPath) $destinationRealPath = $this->_directory->getDriver()->getRealPath($destPath); $relativeDestPath = $this->_directory->getRelativePath($destPath); $isSameFile = $tmpRealPath === $destinationRealPath; - return $isSameFile ?: $this->_directory->copyFile($tmpPath, $relativeDestPath); + $rootDirectory = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + return $isSameFile ?: $this->_directory->copyFile($tmpPath, $relativeDestPath, $rootDirectory); } else { return false; } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index 52769859a74ac..2eb8c86a34686 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -39,6 +39,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File as DriverFile; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Json\Helper\Data; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; @@ -207,6 +208,9 @@ class ProductTest extends AbstractImportTestCase /** @var ImageTypeProcessor|MockObject */ protected $imageTypeProcessor; + /** @var DriverFile|MockObject */ + private $driverFile; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -374,6 +378,10 @@ protected function setUp(): void $this->errorAggregator = $this->getErrorAggregatorObject(); + $this->driverFile = $this->getMockBuilder(DriverFile::class) + ->disableOriginalConstructor() + ->getMock(); + $this->data = []; $this->imageTypeProcessor = $this->getMockBuilder(ImageTypeProcessor::class) @@ -1336,6 +1344,10 @@ public function testFillUploaderObject($isRead, $isWrite, $message) ->with('pub/media/catalog/product') ->willReturn($isWrite); + $this->_mediaDirectory + ->method('getDriver') + ->willReturn($this->driverFile); + $this->_mediaDirectory ->method('getRelativePath') ->willReturnMap( diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index 2d482938949bc..bc8fba5e2b919 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -9,6 +9,7 @@ use Magento\CatalogImportExport\Model\Import\Uploader; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Driver\Http; use Magento\Framework\Filesystem\Driver\Https; @@ -73,6 +74,11 @@ class UploaderTest extends TestCase */ protected $uploader; + /** + * @var TargetDirectory|MockObject + */ + private $targetDirectory; + protected function setUp(): void { $this->coreFileStorageDb = $this->getMockBuilder(Database::class) @@ -115,6 +121,13 @@ protected function setUp(): void ->setMethods(['getRandomString']) ->getMock(); + $this->targetDirectory = $this->getMockBuilder(TargetDirectory::class) + ->disableOriginalConstructor() + ->setMethods(['getDirectoryWrite', 'getDirectoryRead']) + ->getMock(); + $this->targetDirectory->method('getDirectoryWrite')->willReturn($this->directoryMock); + $this->targetDirectory->method('getDirectoryRead')->willReturn($this->directoryMock); + $this->uploader = $this->getMockBuilder(Uploader::class) ->setConstructorArgs( [ @@ -125,7 +138,8 @@ protected function setUp(): void $this->filesystem, $this->readFactory, null, - $this->random + $this->random, + $this->targetDirectory ] ) ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) @@ -274,9 +288,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->addMethods(['readAll']) ->onlyMethods(['isExists']) ->getMock(); - $driverMock->expects($this->any())->method('isExists')->willReturn(true); - $driverMock->expects($this->any())->method('readAll')->willReturn(null); - $driverPool->expects($this->any())->method('getDriver')->willReturn($driverMock); + $driverMock->method('isExists')->willReturn(true); + $driverMock->method('readAll')->willReturn(null); + $driverPool->method('getDriver')->willReturn($driverMock); $readFactory = $this->getMockBuilder(ReadFactory::class) ->setConstructorArgs( @@ -287,10 +301,11 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->setMethods(['create']) ->getMock(); - $readFactory->expects($this->any())->method('create') + $readFactory->method('create') ->with($expectedHost, $expectedScheme) ->willReturn($driverMock); + /** @var Uploader $uploaderMock */ $uploaderMock = $this->getMockBuilder(Uploader::class) ->setConstructorArgs( [ @@ -300,6 +315,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive $this->validator, $this->filesystem, $readFactory, + null, + $this->random, + $this->targetDirectory ] ) ->getMock(); diff --git a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php index e6ead5d5cc021..3450376365cd0 100644 --- a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php +++ b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php @@ -6,6 +6,7 @@ namespace Magento\DownloadableImportExport\Helper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Driver\File; /** * Uploader helper for downloadable products @@ -82,6 +83,11 @@ public function getUploader($type, $parameters) $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + // make media folder a primary folder for media in external storages + if (!is_a($this->mediaDirectory->getDriver(), File::class)) { + $dirAddon = DirectoryList::MEDIA; + } + if (!empty($parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR])) { $tmpPath = $parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR]; } else { @@ -113,7 +119,9 @@ public function getUploader($type, $parameters) */ public function isFileExist(string $fileName): bool { - return $this->mediaDirectory->isExist($this->fileUploader->getDestDir().$fileName); + $fileName = '/' . ltrim($fileName, '/'); + + return $this->mediaDirectory->isExist($this->fileUploader->getDestDir() . $fileName); } /** diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php index 4107e19860328..26ee257c42ff2 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php @@ -67,13 +67,12 @@ public function execute() return $resultRedirect; } try { - $path = 'export/' . $fileName; - $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); - if ($directory->isFile($path)) { + $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_EXPORT); + if ($directory->isFile($fileName)) { return $this->fileFactory->create( - $path, - $directory->readFile($path), - DirectoryList::VAR_DIR + $fileName, + $directory->readFile($fileName), + DirectoryList::VAR_EXPORT ); } $this->messageManager->addErrorMessage(__('%1 is not a valid file', $fileName)); diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php index 27019780269c4..955f96fe3de2e 100644 --- a/app/code/Magento/ImportExport/Model/Export/Consumer.php +++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php @@ -70,8 +70,8 @@ public function process(ExportInfoInterface $exportInfo) try { $data = $this->exportManager->export($exportInfo); $fileName = $exportInfo->getFileName(); - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); - $directory->writeFile('export/' . $fileName, $data); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); + $directory->writeFile($fileName, $data); $this->notifier->addMajor( __('Your export file is ready'), diff --git a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php index 2b5af6ab5ca8d..71614bafd138e 100644 --- a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php +++ b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php @@ -13,6 +13,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Filesystem\Directory\WriteInterface; /** * Data provider for export grid. @@ -29,6 +30,11 @@ class ExportFileDataProvider extends DataProvider */ private $file; + /** + * @var WriteInterface + */ + private $directory; + /** * @var Filesystem */ @@ -48,6 +54,7 @@ class ExportFileDataProvider extends DataProvider * @param array $meta * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( string $name, @@ -78,6 +85,7 @@ public function __construct( ); $this->fileIO = $fileIO ?: ObjectManager::getInstance()->get(File::class); + $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); } /** @@ -88,13 +96,12 @@ public function __construct( */ public function getData() { - $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); $emptyResponse = ['items' => [], 'totalRecords' => 0]; - if (!$this->file->isExists($directory->getAbsolutePath() . 'export/')) { + if (!$this->directory->isExist($this->directory->getAbsolutePath())) { return $emptyResponse; } - $files = $this->getExportFiles($directory->getAbsolutePath() . 'export/'); + $files = $this->getExportFiles($this->directory->getAbsolutePath()); if (empty($files)) { return $emptyResponse; } @@ -121,12 +128,15 @@ public function getData() */ private function getPathToExportFile($file): string { - $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); + $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_EXPORT); $delimiter = '/'; $cutPath = explode( $delimiter, - $directory->getAbsolutePath() . 'export' + $directory->getAbsolutePath() ); + // remove . from dirname if file path is not absolute in the file system but just a file name + $file['dirname'] = $file['dirname'] !== '.' ? $file['dirname'] : ''; + $filePath = explode( $delimiter, $file['dirname'] @@ -148,14 +158,14 @@ private function getPathToExportFile($file): string private function getExportFiles(string $directoryPath): array { $sortedFiles = []; - $files = $this->file->readDirectoryRecursively($directoryPath); + $files = $this->directory->getDriver()->readDirectoryRecursively($directoryPath); if (empty($files)) { return []; } foreach ($files as $filePath) { - if ($this->file->isFile($filePath)) { - //phpcs:ignore Magento2.Functions.DiscouragedFunction - $sortedFiles[filemtime($filePath)] = $filePath; + if ($this->directory->isFile($filePath)) { + $fileModificationTime = $this->directory->stat($filePath)['mtime']; + $sortedFiles[$fileModificationTime] = $filePath; } } //sort array elements using key value diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml index 04ee726349123..7b124957d5f57 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -16,11 +16,13 @@ <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> </arguments> </type> + <!-- deprecated as file argument is not used anymore. Can be deleted in major release to avoid BIC.--> <type name="Magento\ImportExport\Controller\Adminhtml\Export\File\Delete"> <arguments> <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> </arguments> </type> + <!-- deprecated as file argument is not used anymore. Can be deleted in major release to avoid BIC.--> <type name="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider"> <arguments> <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php index 1594e53392b76..f2d5237ea243b 100644 --- a/app/code/Magento/RemoteStorage/Filesystem.php +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -105,4 +105,19 @@ public function getDirectoryWrite($directoryCode, $driverCode = DriverPool::REMO return parent::getDirectoryWrite($directoryCode); } + + /** + * @inheritDoc + */ + public function getDirectoryReadByPath($path, $driverCode = DriverPool::REMOTE) + { + if ($driverCode === DriverPool::REMOTE && $this->isEnabled) { + return $this->readFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $path), + $driverCode + ); + } + + return parent::getDirectoryReadByPath($path); + } } diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php index 8f554a3d8f8c3..66c5fe1a5ac67 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Image.php +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -30,7 +30,7 @@ class Image /** * @var Filesystem\Directory\WriteInterface */ - private $targetDirectoryWrite; + private $remoteDirectoryWrite; /** * @var array @@ -69,7 +69,7 @@ public function __construct( LoggerInterface $logger ) { $this->tmpDirectoryWrite = $filesystem->getDirectoryWrite(DirectoryList::TMP); - $this->targetDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); + $this->remoteDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); $this->isEnabled = $config->isEnabled(); $this->ioFile = $ioFile; $this->logger = $logger; @@ -93,6 +93,42 @@ public function beforeOpen(AbstractAdapter $subject, $filename): array return [$filename]; } + /** + * Copy import file locally to validate + * + * @param AbstractAdapter $subject + * @param string $filePath + * @return string[] + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeValidateUploadFile(AbstractAdapter $subject, $filePath): array + { + if ($this->isEnabled) { + $filePath = $this->copyFileToTmp($filePath); + } + return [$filePath]; + } + + /** + * Copy watermark locally before adding it an image + * + * @param AbstractAdapter $subject + * @param string $filePath + * @return string[] + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeWatermark(AbstractAdapter $subject, $filePath): array + { + if ($this->isEnabled) { + $filePath = $this->copyFileToTmp($filePath); + } + return [$filePath]; + } + /** * Get filesystem tmp path for file and provide it to save() function * @@ -110,16 +146,15 @@ public function aroundSave( $newName = null ): void { if ($this->isEnabled) { - $relativePath = $this->targetDirectoryWrite->getRelativePath($destination); + $relativePath = $this->remoteDirectoryWrite->getRelativePath($destination); $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath($relativePath); $proceed($tmpPath, $newName); - $destination = $this->prepareDestination($subject, $destination, $newName); $this->tmpDirectoryWrite->getDriver()->rename( - $tmpPath, - $destination, - $this->targetDirectoryWrite->getDriver() + $this->prepareDestination($subject, $tmpPath, $newName), + $this->prepareDestination($subject, $destination, $newName), + $this->remoteDirectoryWrite->getDriver() ); } else { $proceed($destination, $newName); @@ -149,13 +184,14 @@ public function __destruct() */ private function copyFileToTmp($filePath): string { - $absolutePath = $this->targetDirectoryWrite->getAbsolutePath($filePath); - if ($this->targetDirectoryWrite->isFile($absolutePath)) { + if ($this->fileExistsInTmp($filePath)) { + return $filePath; + } + $absolutePath = $this->remoteDirectoryWrite->getAbsolutePath($filePath); + if ($this->remoteDirectoryWrite->isFile($absolutePath)) { $this->tmpDirectoryWrite->create(); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); - $this->storeTmpName($tmpPath); - $content = $this->targetDirectoryWrite->getDriver()->fileGetContents($filePath); + $tmpPath = $this->storeTmpName($filePath); + $content = $this->remoteDirectoryWrite->getDriver()->fileGetContents($filePath); $filePath = $this->tmpDirectoryWrite->getDriver()->filePutContents($tmpPath, $content) ? $tmpPath : $filePath; @@ -166,11 +202,28 @@ private function copyFileToTmp($filePath): string /** * Store created tmp image path * - * @param string $path + * @param string $filePath + * @return string + */ + private function storeTmpName(string $filePath): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); + + $this->tmpFiles[$filePath] = $tmpPath; + + return $tmpPath; + } + + /** + * Check is file exist in tmp folder + * + * @param string $filePath + * @return bool */ - private function storeTmpName(string $path): void + private function fileExistsInTmp(string $filePath): bool { - $this->tmpFiles[] = $path; + return in_array($filePath, $this->tmpFiles, true); } /** diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php index 4055422a8aa4e..13d170946e343 100644 --- a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php +++ b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\RemoteStorage\Test\Unit\Plugin; use Magento\Framework\App\Filesystem\DirectoryList; @@ -41,8 +42,8 @@ class ImageTest extends TestCase private $targetDirectoryWrite; /** - * @throws \Magento\Framework\Exception\FileSystemException * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ protected function setUp(): void { @@ -80,11 +81,16 @@ protected function setUp(): void * @param string $destination * @param string $newDestination * @param string|null $newName + * @param string|null $oldName * @return void * @throws \Magento\Framework\Exception\FileSystemException */ - public function testAroundSaveWithNewName(string $destination, string $newDestination, ?string $newName): void - { + public function testAroundSaveWithNewName( + string $destination, + string $newDestination, + ?string $newName, + ?string $oldName + ): void { $tmpDestination = '/tmp/' . $destination; /** @var AbstractAdapter $subject */ $subject = $this->getMockBuilder(AbstractAdapter::class) @@ -96,7 +102,7 @@ public function testAroundSaveWithNewName(string $destination, string $newDestin ->disableOriginalConstructor() ->getMock(); $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getRelativePath') - ->willReturn($destination); + ->willReturn($destination . $oldName); $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') ->willReturn($targetDriver); $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') @@ -104,12 +110,18 @@ public function testAroundSaveWithNewName(string $destination, string $newDestin $driver = $this->getMockBuilder(DriverInterface::class) ->disableOriginalConstructor() ->getMock(); + $actualName = $newName ?? $oldName; $driver->expects(self::atLeastOnce())->method('rename') - ->with($tmpDestination, $newDestination, $driver); + ->with($tmpDestination . $actualName, $newDestination, $driver); $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver')->willReturn($driver); - $this->ioFile->expects(self::any())->method('getPathInfo')->with($destination) - ->willReturn(['dirname' => 'destination/', 'basename' => 'old_name.file']); - $this->plugin->aroundSave($subject, $proceed, $destination, $newName); + $this->ioFile->method('getPathInfo') + ->willReturnMap( + [ + [$tmpDestination, ['dirname' => $tmpDestination, 'basename' => 'old_name.file']], + [$destination . $oldName, ['dirname' => $destination, 'basename' => 'old_name.file']] + ] + ); + $this->plugin->aroundSave($subject, $proceed, $destination . $oldName, $newName); } /** @@ -121,12 +133,14 @@ public function aroundSaveDataProvider(): array 'with_new_name' => [ 'destination' => 'destination/', 'new_destination' => 'destination/new_name.file', - 'new_name' => 'new_name.file' + 'new_name' => 'new_name.file', + 'old_name' => null ], 'with_old_name' => [ - 'destination' => 'destination/old_name.file', + 'destination' => 'destination/', 'new_destination' => 'destination/old_name.file', - 'new_name' => null + 'new_name' => null, + 'old_name' => 'old_name.file' ] ]; } diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index c55923f6e2109..fa27e821c817c 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -10,7 +10,10 @@ "magento/module-sitemap": "*", "magento/module-cms": "*", "magento/module-downloadable": "*", - "magento/module-media-storage": "*" + "magento/module-media-storage": "*", + "magento/module-import-export": "*", + "magento/module-catalog-import-export": "*", + "magento/module-downloadable-import-export": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index fe16d1d4afca5..bb253bb5d18f7 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -26,6 +26,8 @@ <arguments> <argument name="directoryCodes" xsi:type="array"> <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> + <item name="var_export" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::VAR_EXPORT</item> + <item name="var_import" xsi:type="string">Magento\Framework\App\Filesystem\DirectoryList::VAR_IMPORT</item> </argument> </arguments> </virtualType> @@ -95,4 +97,34 @@ <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> </type> + <type name="Magento\ImportExport\Model\Import"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Model\Import\ImageDirectoryBaseProvider"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Helper\Report"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\CatalogImportExport\Model\Import\Product"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\CatalogImportExport\Model\Import\Uploader"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\DownloadableImportExport\Helper\Uploader"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php index d7b492bf5153c..7bd4b3a99d1bf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php @@ -8,7 +8,6 @@ namespace Magento\Framework\App\Filesystem; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Response\Http\FileFactory; use Magento\Framework\Filesystem; use Magento\TestFramework\Helper\Bootstrap; diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php index 277e6af871650..2128516189474 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php @@ -105,7 +105,7 @@ public function testExecute($file): void 'Incorrect response header "content-type"' ); $this->assertEquals( - 'attachment; filename="export/' . $this->fileName . '"', + 'attachment; filename="' . $this->fileName . '"', $contentDisposition->getFieldValue(), 'Incorrect response header "content-disposition"' ); diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index 295ac50cf5687..fdf524348293b 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -62,6 +62,11 @@ class DirectoryList extends \Magento\Framework\Filesystem\DirectoryList */ const VAR_EXPORT = 'var_export'; + /** + * Storage of files which were imported. + */ + const VAR_IMPORT = 'var_import'; + /** * Temporary files */ @@ -151,7 +156,7 @@ public static function getDefaultConfig() self::CONFIG => [parent::PATH => 'app/etc'], self::LIB_INTERNAL => [parent::PATH => 'lib/internal'], self::VAR_DIR => [parent::PATH => 'var'], - self::VAR_EXPORT => [parent::PATH => 'var/export'], + self::VAR_EXPORT => [parent::PATH => 'var/export', parent::URL_PATH => 'export'], self::CACHE => [parent::PATH => 'var/cache'], self::LOG => [parent::PATH => 'var/log'], self::DI => [parent::PATH => 'generated/metadata'], @@ -170,6 +175,7 @@ public static function getDefaultConfig() self::GENERATED => [parent::PATH => 'generated'], self::GENERATED_CODE => [parent::PATH => Io::DEFAULT_DIRECTORY], self::GENERATED_METADATA => [parent::PATH => 'generated/metadata'], + self::VAR_IMPORT => [parent::PATH => 'var/import', parent::URL_PATH => 'var/import'], ]; return parent::getDefaultConfig() + $result; } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 0ff25f868d7af..4d4bb3b6c7f5e 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -349,7 +349,11 @@ public function openFile($path, $mode = 'w') */ public function writeFile($path, $content, $mode = 'w+') { - return $this->openFile($path, $mode)->write($content); + $file = $this->openFile($path, $mode); + $result = $file->write($content); + $file->close(); + + return $result; } /** diff --git a/lib/internal/Magento/Framework/Image.php b/lib/internal/Magento/Framework/Image.php index a14f94b8f2733..5b49e9f303ca0 100644 --- a/lib/internal/Magento/Framework/Image.php +++ b/lib/internal/Magento/Framework/Image.php @@ -90,7 +90,7 @@ public function rotate($angle) /** * Crop an image. * - * @param int $top Default value is 0 + * @param int $top Default value is 0 * @param int $left Default value is 0 * @param int $right Default value is 0 * @param int $bottom Default value is 0 @@ -200,9 +200,6 @@ public function watermark( $watermarkImageOpacity = 30, $repeat = false ) { - if (!file_exists($watermarkImage)) { - throw new \Exception("Required file '{$watermarkImage}' does not exists."); - } $this->_adapter->watermark($watermarkImage, $positionX, $positionY, $watermarkImageOpacity, $repeat); } @@ -234,7 +231,7 @@ public function getImageType() * @access public * @return void */ - public function process() + public function process() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } @@ -244,7 +241,7 @@ public function process() * @access public * @return void */ - public function instruction() + public function instruction() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index c37cb89c30587..bebf64c56596a 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -6,6 +6,9 @@ namespace Magento\Framework\Image\Adapter; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Phrase; + /** * Gd2 adapter. * @@ -56,10 +59,15 @@ protected function _reset() * * @param string $filename * @return void - * @throws \OverflowException + * @throws \OverflowException|FileSystemException */ public function open($filename) { + if (!file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$this->_fileName]) + ); + } if (!$filename || filesize($filename) === 0 || !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } @@ -436,8 +444,6 @@ public function rotate($angle) * @param int $opacity * @param bool $tile * @return void - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -452,53 +458,42 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $merged = false; + $watermark = $this->createWatermarkBasedOnPosition($watermark, $positionX, $positionY, $merged, $tile); + + imagedestroy($watermark); + $this->refreshImageDimensions(); + } + + /** + * Create watermark based on it's image position. + * + * @param resource $watermark + * @param int $positionX + * @param int $positionY + * @param bool $merged + * @param bool $tile + * @return false|resource + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function createWatermarkBasedOnPosition( + $watermark, + int $positionX, + int $positionY, + bool $merged, + bool $tile + ) { if ($this->getWatermarkWidth() && $this->getWatermarkHeight() && $this->getWatermarkPosition() != self::POSITION_STRETCH ) { - $newWatermark = imagecreatetruecolor($this->getWatermarkWidth(), $this->getWatermarkHeight()); - imagealphablending($newWatermark, false); - $col = imagecolorallocate($newWatermark, 255, 255, 255); - imagecolortransparent($newWatermark, $col); - imagefilledrectangle($newWatermark, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeight(), $col); - imagesavealpha($newWatermark, true); - imagecopyresampled( - $newWatermark, - $watermark, - 0, - 0, - 0, - 0, - $this->getWatermarkWidth(), - $this->getWatermarkHeight(), - imagesx($watermark), - imagesy($watermark) - ); - $watermark = $newWatermark; + $watermark = $this->createWaterMark($watermark, $this->getWatermarkWidth(), $this->getWatermarkHeight()); } if ($this->getWatermarkPosition() == self::POSITION_TILE) { $tile = true; } elseif ($this->getWatermarkPosition() == self::POSITION_STRETCH) { - $newWatermark = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); - imagealphablending($newWatermark, false); - $col = imagecolorallocate($newWatermark, 255, 255, 255); - imagecolortransparent($newWatermark, $col); - imagefilledrectangle($newWatermark, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, $col); - imagesavealpha($newWatermark, true); - imagecopyresampled( - $newWatermark, - $watermark, - 0, - 0, - 0, - 0, - $this->_imageSrcWidth, - $this->_imageSrcHeight, - imagesx($watermark), - imagesy($watermark) - ); - $watermark = $newWatermark; + $watermark = $this->createWaterMark($watermark, $this->_imageSrcWidth, $this->_imageSrcHeight); } elseif ($this->getWatermarkPosition() == self::POSITION_CENTER) { $positionX = $this->_imageSrcWidth / 2 - imagesx($watermark) / 2; $positionY = $this->_imageSrcHeight / 2 - imagesy($watermark) / 2; @@ -602,8 +597,39 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = } } - imagedestroy($watermark); - $this->refreshImageDimensions(); + return $watermark; + } + + /** + * Create watermark. + * + * @param resource $watermark + * @param string $width + * @param string $height + * @return false|resource + */ + private function createWaterMark($watermark, string $width, string $height) + { + $newWatermark = imagecreatetruecolor($width, $height); + imagealphablending($newWatermark, false); + $col = imagecolorallocate($newWatermark, 255, 255, 255); + imagecolortransparent($newWatermark, $col); + imagefilledrectangle($newWatermark, 0, 0, $width, $height, $col); + imagesavealpha($newWatermark, true); + imagecopyresampled( + $newWatermark, + $watermark, + 0, + 0, + 0, + 0, + $width, + $height, + imagesx($watermark), + imagesy($watermark) + ); + + return $newWatermark; } /** diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 7e36cdb334eb2..31793d281ac52 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -6,7 +6,9 @@ namespace Magento\Framework\Image\Adapter; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; /** * Image adapter from ImageMagick. @@ -88,6 +90,11 @@ public function backgroundColor($color = null) */ public function open($filename) { + if (!file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$this->_fileName]) + ); + } if (!empty($filename) && !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php index f21101f099200..355a221c5d368 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -80,7 +80,7 @@ public function watermarkDataProvider(): array { return [ ['', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], - [__DIR__ . '/not_exists', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], + ['not_exist', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], [ __DIR__ . '/_files/invalid_image.jpg', ImageMagick::ERROR_WRONG_IMAGE @@ -105,6 +105,8 @@ public function testSaveWithException() */ public function testOpenInvalidUrl() { + require_once __DIR__ . '/_files/global_php_mock.php'; + $this->expectException(\InvalidArgumentException::class); $this->imageMagic->open('bar://foo.bar'); diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php index a62909b495ab4..034e9c32c6954 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php @@ -48,6 +48,16 @@ function filesize($file) return Gd2Test::$imageSize; } +/** + * @param $file + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function file_exists($file) +{ + return !($file === 'not_exist'); +} + /** * @param $real * @return int From 30fb5fa51ed427a6fa29f5032cf515335ea50b3e Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Mon, 26 Oct 2020 17:08:56 -0500 Subject: [PATCH 116/195] MC-38698: [Magento Cloud] Detected outage on node 1 of the cluster --- app/code/Magento/Sales/etc/db_schema.xml | 3 +++ app/code/Magento/Sales/etc/db_schema_whitelist.json | 1 + 2 files changed, 4 insertions(+) diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index ab524a0f552f6..491772e7e65a0 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -825,6 +825,9 @@ <index referenceId="SALES_SHIPMENT_GRID_BILLING_NAME" indexType="btree"> <column name="billing_name"/> </index> + <index referenceId="SALES_SHIPMENT_GRID_ORDER_ID" indexType="btree"> + <column name="order_id"/> + </index> <index referenceId="FTI_086B40C8955F167B8EA76653437879B4" indexType="fulltext"> <column name="increment_id"/> <column name="order_increment_id"/> diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json index 087fe6c9eb5ac..02efd7d5a0050 100644 --- a/app/code/Magento/Sales/etc/db_schema_whitelist.json +++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json @@ -479,6 +479,7 @@ "SALES_SHIPMENT_GRID_ORDER_CREATED_AT": true, "SALES_SHIPMENT_GRID_SHIPPING_NAME": true, "SALES_SHIPMENT_GRID_BILLING_NAME": true, + "SALES_SHIPMENT_GRID_ORDER_ID": true, "FTI_086B40C8955F167B8EA76653437879B4": true }, "constraint": { From 0761e1ad760d103090100664d21af055ca8d21ee Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 27 Oct 2020 12:16:00 +0200 Subject: [PATCH 117/195] MC-38663: Korean postal code is now 5 digit not 6 --- app/code/Magento/Directory/etc/zip_codes.xml | 1 + .../Model/Country/Postcode/Config/ReaderTest.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/code/Magento/Directory/etc/zip_codes.xml b/app/code/Magento/Directory/etc/zip_codes.xml index de6c064815d7a..634d4abe06763 100644 --- a/app/code/Magento/Directory/etc/zip_codes.xml +++ b/app/code/Magento/Directory/etc/zip_codes.xml @@ -229,6 +229,7 @@ <zip countryCode="KR"> <codes> <code id="pattern_1" active="true" example="123-456">^[0-9]{3}-[0-9]{3}$</code> + <code id="pattern_2" active="true" example="12345">^[0-9]{5}$</code> </codes> </zip> <zip countryCode="KG"> diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php index 9146535ed5181..87dfd2a4a3981 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php @@ -49,5 +49,13 @@ public function testRead() $this->assertEquals('^[0-9]{4}$', $result['AR']['pattern_1']['pattern']); $this->assertEquals('A1234BCD', $result['AR']['pattern_2']['example']); $this->assertEquals('^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$', $result['AR']['pattern_2']['pattern']); + + $this->assertArrayHasKey('KR', $result); + $this->assertArrayHasKey('pattern_1', $result['KR']); + $this->assertArrayHasKey('pattern_2', $result['KR']); + $this->assertEquals('123-456', $result['KR']['pattern_1']['example']); + $this->assertEquals('^[0-9]{3}-[0-9]{3}$', $result['KR']['pattern_1']['pattern']); + $this->assertEquals('12345', $result['KR']['pattern_2']['example']); + $this->assertEquals('^[0-9]{5}$', $result['KR']['pattern_2']['pattern']); } } From 5ba03c224aa3ea69299a60a43e09f7fd6cd9c692 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Tue, 27 Oct 2020 13:15:17 +0200 Subject: [PATCH 118/195] magento/magento2#30372: [Issue] MFTF: Admin Delete CMS Block Test. --- .../Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml | 2 +- .../Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml index a9c9a5943529c..f558619fa49ac 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -14,6 +14,6 @@ <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> - <element name="gridDataRow" type="input" selector=".data-row .data-grid-cell-content"/> + <element name="gridDataRow" type="input" selector="//table[@data-role='grid']//tr/td"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index 529000dc44c3a..38281d4d6d1d6 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -20,7 +20,7 @@ <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> <element name="blockGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> - <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> - <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> + <element name="delete" type="button" selector="//a[@data-action='item-delete']"/> + <element name="deleteConfirm" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="60"/> </section> </sections> From 00a170a6a783d7d93e05d625548fb7aff4e1b4d1 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <duhon@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:03:00 -0500 Subject: [PATCH 119/195] [performance] MC-38137 Custom option (#6275) --- .../Magento/Bundle/Model/Product/Type.php | 11 +++-- .../Option/Type/File/ValidatorFile.php | 13 +++--- .../Model/Product/Type/AbstractType.php | 44 +++++++++---------- .../Model/Product/Type/Configurable.php | 14 +++--- .../Downloadable/Model/Product/Type.php | 10 +++-- .../Model/Product/Type/Grouped.php | 8 +++- .../Option/Type/File/ValidatorFileTest.php | 4 +- 7 files changed, 57 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index fe120e9a179dd..6ee67859db015 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -13,6 +13,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\File\UploaderFactory; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\ArrayUtils; @@ -190,11 +191,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param PriceCurrencyInterface $priceCurrency * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\CatalogInventory\Api\StockStateInterface $stockState - * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param Json|null $serializer * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier * @param ArrayUtils|null $arrayUtility - * + * @param UploaderFactory $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -222,7 +223,8 @@ public function __construct( Json $serializer = null, MetadataPool $metadataPool = null, SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, - ArrayUtils $arrayUtility = null + ArrayUtils $arrayUtility = null, + UploaderFactory $uploaderFactory = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -254,7 +256,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php index fef4999a1174a..934ff48045097 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Math\Random; use Magento\Framework\App\ObjectManager; +use Magento\MediaStorage\Model\File\Uploader; /** * Validator class. Represents logic for validation file given from product option @@ -173,15 +174,11 @@ public function validate($processingParams, $option) $userValue = []; if ($upload->isUploaded($file) && $upload->isValid($file)) { - $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']); - $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); - - $filePath = $dispersion; - $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); - $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); $fileRandomName = $this->random->getRandomString(32); - $filePath .= '/' .$fileRandomName; + $fileName = Uploader::getCorrectFileName($fileRandomName); + $dispersion = Uploader::getDispersionPath($fileName); + $filePath = $dispersion . '/' . $fileName; $fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath); $upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true])); @@ -216,6 +213,8 @@ public function validate($processingParams, $option) } } + $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); + $userValue = [ 'type' => $fileInfo['type'], 'title' => $fileInfo['name'], diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index eb4a71cb90a8c..e14a38e61a25f 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -7,9 +7,9 @@ namespace Magento\Catalog\Model\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\UploaderFactory; /** * Abstract model for product type implementation @@ -113,6 +113,11 @@ abstract class AbstractType */ protected $_cacheProductSetAttributes = '_cache_instance_product_set_attributes'; + /** + * @var UploaderFactory + */ + private $uploaderFactory; + /** * Delete data specific for this product type * @@ -175,8 +180,6 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ protected $serializer; /** - * Construct - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -187,6 +190,7 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ * @param \Psr\Log\LoggerInterface $logger * @param ProductRepositoryInterface $productRepository * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -199,7 +203,8 @@ public function __construct( \Magento\Framework\Registry $coreRegistry, \Psr\Log\LoggerInterface $logger, ProductRepositoryInterface $productRepository, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->_catalogProductOption = $catalogProductOption; $this->_eavConfig = $eavConfig; @@ -212,6 +217,7 @@ public function __construct( $this->productRepository = $productRepository; $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->uploaderFactory = $uploaderFactory ?: ObjectManager::getInstance()->get(UploaderFactory::class); } /** @@ -493,28 +499,20 @@ public function processFileQueue() if (isset($queueOptions['operation']) && ($operation = $queueOptions['operation'])) { switch ($operation) { case 'receive_uploaded_file': - $src = isset($queueOptions['src_name']) ? $queueOptions['src_name'] : ''; - $dst = isset($queueOptions['dst_name']) ? $queueOptions['dst_name'] : ''; + $src = $queueOptions['src_name'] ?? ''; + $dst = $queueOptions['dst_name'] ?? ''; /** @var $uploader \Zend_File_Transfer_Adapter_Http */ - $uploader = isset($queueOptions['uploader']) ? $queueOptions['uploader'] : null; - - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $path = dirname($dst); - - try { - $rootDir = $this->_filesystem->getDirectoryWrite( - DirectoryList::ROOT - ); - $rootDir->create($rootDir->getRelativePath($path)); - } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException( - __('We can\'t create the "%1" writeable directory.', $path) - ); + $uploader = $queueOptions['uploader'] ?? null; + $isUploaded = false; + if ($uploader && $uploader->isValid()) { + $path = pathinfo($dst, PATHINFO_DIRNAME); + $uploader = $this->uploaderFactory->create(['fileId' => $src]); + $uploader->setFilesDispersion(false); + $uploader->setAllowRenameFiles(true); + $isUploaded = $uploader->save($path, pathinfo($dst, PATHINFO_FILENAME)); } - $uploader->setDestination($path); - - if (empty($src) || empty($dst) || !$uploader->receive($src)) { + if (empty($src) || empty($dst) || !$isUploaded) { /** * @todo: show invalid option */ diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index c2ae381b345c6..79f6d1e47f1a2 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -17,6 +17,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\File\UploaderFactory; /** * Configurable product type implementation @@ -235,11 +236,12 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor * @param \Magento\Framework\Cache\FrontendInterface|null $cache * @param \Magento\Customer\Model\Session|null $customerSession - * @param \Magento\Framework\Serialize\Serializer\Json $serializer - * @param ProductInterfaceFactory $productFactory - * @param SalableProcessor $salableProcessor + * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param ProductInterfaceFactory|null $productFactory + * @param SalableProcessor|null $salableProcessor * @param ProductAttributeRepositoryInterface|null $productAttributeRepository * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -266,7 +268,8 @@ public function __construct( ProductInterfaceFactory $productFactory = null, SalableProcessor $salableProcessor = null, ProductAttributeRepositoryInterface $productAttributeRepository = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null + SearchCriteriaBuilder $searchCriteriaBuilder = null, + UploaderFactory $uploaderFactory = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -295,7 +298,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/Downloadable/Model/Product/Type.php b/app/code/Magento/Downloadable/Model/Product/Type.php index cb79dda3baccb..45a03b50d78b8 100644 --- a/app/code/Magento/Downloadable/Model/Product/Type.php +++ b/app/code/Magento/Downloadable/Model/Product/Type.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\File\UploaderFactory; /** * Downloadable product type model @@ -67,8 +68,6 @@ class Type extends \Magento\Catalog\Model\Product\Type\Virtual private $extensionAttributesJoinProcessor; /** - * Construct - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -87,6 +86,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\Virtual * @param TypeHandler\TypeHandlerInterface $typeHandler * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -107,7 +107,8 @@ public function __construct( \Magento\Downloadable\Model\LinkFactory $linkFactory, \Magento\Downloadable\Model\Product\TypeHandler\TypeHandlerInterface $typeHandler, JoinProcessorInterface $extensionAttributesJoinProcessor, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->_sampleResFactory = $sampleResFactory; $this->_linkResource = $linkResource; @@ -127,7 +128,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index 8eac8d0b0e163..b56e8657df722 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -8,6 +8,7 @@ namespace Magento\GroupedProduct\Model\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\File\UploaderFactory; /** * Grouped product type model @@ -102,6 +103,7 @@ class Grouped extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\App\State $appState * @param \Magento\Msrp\Helper\Data $msrpData * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -119,7 +121,8 @@ public function __construct( \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, \Magento\Framework\App\State $appState, \Magento\Msrp\Helper\Data $msrpData, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->productLinks = $catalogProductLink; $this->_storeManager = $storeManager; @@ -136,7 +139,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php index 0be889f546a2b..64b009b5b8d13 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php @@ -362,8 +362,8 @@ protected function expectedValidate() return [ 'type' => 'image/jpeg', 'title' => 'test.jpg', - 'quote_path' => 'custom_options/quote/t/e/RandomString', - 'order_path' => 'custom_options/order/t/e/RandomString', + 'quote_path' => 'custom_options/quote/R/a/RandomString', + 'order_path' => 'custom_options/order/R/a/RandomString', 'size' => '3046', 'width' => 136, 'height' => 131, From de8edcfd37f42f2001c856320597aed9b4161ac6 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Tue, 27 Oct 2020 20:01:40 -0500 Subject: [PATCH 120/195] [AWS S3] Improvements (#6262) --- .gitignore | 2 + app/code/Magento/AwsS3/Driver/AwsS3.php | 66 +++++- .../Magento/AwsS3/Driver/AwsS3Factory.php | 14 +- .../AwsS3/Test/Mftf/Data/ConfigData.xml | 2 + .../AwsS3AdminAddImageForCategoryTest.xml | 27 +++ .../AwsS3AdminAddImageToWYSIWYGBlockTest.xml | 66 +----- .../AwsS3AdminAddImageToWYSIWYGCMSTest.xml | 61 +----- ...S3AdminAddImageToWYSIWYGNewsletterTest.xml | 28 +++ ...AddRemoveDefaultVideoSimpleProductTest.xml | 30 +++ ...nCreateDownloadableProductWithLinkTest.xml | 7 +- ...3AdminMarketingCreateSitemapEntityTest.xml | 47 +--- ...wsS3AdminMarketingSiteMapCreateNewTest.xml | 23 +- .../Mftf/Test/AwsS3CheckingRMAPrintTest.xml | 26 +++ ...tChildImageShouldBeShownOnWishListTest.xml | 29 +++ .../AwsS3StorefrontPrintOrderGuestTest.xml | 30 +++ ...S3UpdateImageFileCustomerAttributeTest.xml | 27 +++ app/code/Magento/AwsS3/composer.json | 3 + .../Catalog/Model/Category/FileInfo.php | 3 +- app/code/Magento/Catalog/etc/config.xml | 2 - .../Cms/Model/Wysiwyg/Images/Storage.php | 9 +- app/code/Magento/Cms/etc/config.xml | 1 + .../Model/CreateAssetFromFile.php | 36 +++- app/code/Magento/MediaStorage/App/Media.php | 50 ++++- .../MediaStorage/Model/File/Storage.php | 13 +- .../MediaStorage/Test/Unit/App/MediaTest.php | 189 ++++++++-------- .../Command/RemoteStorageEnableCommand.php | 12 +- .../RemoteStorage/Driver/DriverException.php | 17 ++ .../Driver/DriverFactoryInterface.php | 6 +- .../Driver/DriverFactoryPool.php | 4 +- .../RemoteStorage/Driver/DriverPool.php | 4 +- .../Driver/RemoteDriverInterface.php | 23 ++ .../Magento/RemoteStorage/Model/Config.php | 33 +-- .../Magento/RemoteStorage/Plugin/Image.php | 2 +- .../RemoteStorage/Plugin/MediaStorage.php | 37 ++-- .../Magento/RemoteStorage/Plugin/Scope.php | 2 +- .../Magento/RemoteStorage/Plugin/Sitemap.php | 67 ------ .../RemoteStorage/Setup/ConfigOptionsList.php | 201 ++++++++++++++++++ app/code/Magento/RemoteStorage/composer.json | 1 + .../RemoteStorage/etc/adminhtml/system.xml | 18 ++ app/code/Magento/RemoteStorage/etc/di.xml | 14 +- app/code/Magento/Sitemap/etc/config.xml | 7 + app/code/Magento/Sitemap/etc/di.xml | 12 ++ .../Theme/Model/Design/Backend/File.php | 2 +- .../Framework/Css/_files/css/test-input.html | 6 + .../Magento/Framework/Config/DocumentRoot.php | 2 +- .../Framework/Data/Collection/Filesystem.php | 8 +- lib/internal/Magento/Framework/File/Mime.php | 175 ++++++--------- .../Framework/File/Test/Unit/MimeTest.php | 97 +++++++-- .../Framework/Filesystem/Driver/File.php | 29 ++- .../Framework/Filesystem/Driver/File/Mime.php | 163 ++++++++++++++ .../Filesystem/ExtendedDriverInterface.php | 43 ++++ .../Test/Unit/Driver/File/MimeTest.php | 67 ++++++ .../Unit/Driver/File/_files/UPPERCASE.WEIRD | 1 + .../Test/Unit/Driver/File/_files/blank.html | 6 + .../Test/Unit/Driver/File/_files/file.weird | 1 + .../Unit/Driver/File/_files/javascript.js | 5 + .../Test/Unit/Driver/File/_files/magento | Bin 0 -> 55303 bytes nginx.conf.sample | 1 + pub/get.php | 13 +- pub/media/sitemap/.htaccess | 7 + 60 files changed, 1287 insertions(+), 590 deletions(-) create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverException.php create mode 100644 app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php delete mode 100644 app/code/Magento/RemoteStorage/Plugin/Sitemap.php create mode 100644 app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php create mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/system.xml create mode 100644 lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php create mode 100644 lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento create mode 100644 pub/media/sitemap/.htaccess diff --git a/.gitignore b/.gitignore index 8ec1104f25535..7092a568ba2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ atlassian* /pub/media/tmp/* !/pub/media/tmp/.htaccess /pub/media/captcha/* +/pub/media/sitemap/* +!/pub/media/sitemap/.htaccess /pub/static/* !/pub/static/.htaccess diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 169a93580038c..5dc1f7e8cb216 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -7,24 +7,29 @@ namespace Magento\AwsS3\Driver; +use Exception; use League\Flysystem\AwsS3v3\AwsS3Adapter; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Phrase; use Psr\Log\LoggerInterface; +use Magento\RemoteStorage\Driver\DriverException; +use Magento\RemoteStorage\Driver\RemoteDriverInterface; /** * Driver for AWS S3 IO operations. * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class AwsS3 implements DriverInterface +class AwsS3 implements RemoteDriverInterface { public const TYPE_DIR = 'dir'; public const TYPE_FILE = 'file'; - private const CONFIG = ['ACL' => 'public-read']; + private const TEST_FLAG = 'storage.flag'; + + private const CONFIG = ['ACL' => 'private']; /** * @var AwsS3Adapter @@ -68,6 +73,18 @@ public function __destruct() } } + /** + * @inheritDoc + */ + public function test(): void + { + try { + $this->adapter->write(self::TEST_FLAG, '', new Config(self::CONFIG)); + } catch (Exception $exception) { + throw new DriverException(__($exception->getMessage()), $exception); + } + } + /** * @inheritDoc */ @@ -177,7 +194,7 @@ public function deleteDirectory($path): bool /** * @inheritDoc */ - public function filePutContents($path, $content, $mode = null, $context = null): int + public function filePutContents($path, $content, $mode = null): int { $path = $this->normalizeRelativePath($path); @@ -345,6 +362,7 @@ public function isDirectory($path): bool if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { return ($meta['type'] ?? null) === self::TYPE_DIR; } + return false; } @@ -416,10 +434,44 @@ public function stat($path): array 'blksize' => 0, 'blocks' => 0, 'size' => $metaInfo['size'] ?? 0, - 'type' => $metaInfo['type'] ?? 0, + 'type' => $metaInfo['type'] ?? '', 'mtime' => $metaInfo['timestamp'] ?? 0, - 'disposition' => null, - 'mimetype' => $metaInfo['mimetype'] ?? 0 + 'disposition' => null + ]; + } + + /** + * @inheritDoc + */ + public function getMetadata(string $path): array + { + $path = $this->normalizeRelativePath($path); + $metaInfo = $this->adapter->getMetadata($path); + + if (!$metaInfo) { + throw new FileSystemException(__('Cannot gather meta info! %1', [$this->getWarningMessage()])); + } + + $extra = [ + 'image-width' => 0, + 'image-height' => 0 + ]; + + if (isset($metaInfo['image-width'], $metaInfo['image-height'])) { + $extra['image-width'] = $metaInfo['image-width']; + $extra['image-height'] = $metaInfo['image-height']; + } + + return [ + 'path' => $metaInfo['path'], + 'dirname' => $metaInfo['dirname'], + 'basename' => $metaInfo['basename'], + 'extension' => $metaInfo['extension'], + 'filename' => $metaInfo['filename'], + 'timestamp' => $metaInfo['timestamp'], + 'size' => $metaInfo['size'], + 'mimetype' => $metaInfo['mimetype'], + 'extra' => $extra ]; } @@ -778,6 +830,7 @@ private function getSearchPattern(string $pattern, array $parentPattern, string '/\?/' => '.', '/\//' => '\/' ]; + return preg_replace(array_keys($replacement), array_values($replacement), $searchPattern); } @@ -816,6 +869,7 @@ private function getDirectoryContent( } } } + return $directoryContent; } } diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index d9efe6f7fd10e..2042e10090407 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -9,9 +9,9 @@ use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; -use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\ObjectManagerInterface; use Magento\RemoteStorage\Driver\DriverFactoryInterface; +use Magento\RemoteStorage\Driver\RemoteDriverInterface; /** * Creates a pre-configured instance of AWS S3 driver. @@ -36,13 +36,15 @@ public function __construct(ObjectManagerInterface $objectManager) * * @param array $config * @param string $prefix - * @return DriverInterface + * @return RemoteDriverInterface */ - public function create(array $config, string $prefix): DriverInterface + public function create(array $config, string $prefix): RemoteDriverInterface { - $config += [ - 'version' => 'latest' - ]; + $config['version'] = 'latest'; + + if (empty($config['credentials']['key']) || empty($config['credentials']['secret'])) { + unset($config['credentials']); + } return $this->objectManager->create( AwsS3::class, diff --git a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml index bc43aff37a491..23be7918106ee 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml @@ -13,5 +13,7 @@ <data key="bucket">{{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}}</data> <data key="access_key">{{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}}</data> <data key="secret_key">{{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}}</data> + <data key="enable_options">--remote-storage-driver={{_ENV.REMOTE_STORAGE_AWSS3_DRIVER}} --remote-storage-bucket={{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}} --remote-storage-region={{_ENV.REMOTE_STORAGE_AWSS3_REGION}} --remote-storage-prefix={{_ENV.REMOTE_STORAGE_AWSS3_PREFIX}} --remote-storage-key={{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}} --remote-storage-secret={{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}} -n</data> + <data key="disable_options">--remote-storage-driver=file -n</data> </entity> </entities> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml new file mode 100644 index 0000000000000..a8f0d4da9e338 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml @@ -0,0 +1,27 @@ +<?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="AwsS3AdminAddImageForCategoryTest" extends="AdminAddImageForCategoryTest"> + <annotations> + <title value="AWS S3 Admin should be able to add image to a Category"/> + <stories value="Add/remove images and videos for all product types and category"/> + <description value="Admin should be able to add image to a Category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38688"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml index 54b7795a84cd3..13e0dcbf41c01 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml @@ -7,74 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminAddImageToWYSIWYGBlockTest"> + <test name="AwsS3AdminAddImageToWYSIWYGBlockTest" extends="AdminAddImageToWYSIWYGBlockTest"> <annotations> - <features value="Cms"/> - <stories value="MC-37460: Support by Magento CMS"/> - <group value="Cms"/> - <title value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> - <description value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> + <description value="Admin should be able to add image to WYSIWYG content of Block"/> <severity value="BLOCKER"/> <testCaseId value="MC-38302"/> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> - <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> - <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> - <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> - <actionGroup ref="AssignBlockToCMSPage" stepKey="assignBlockToCMSPage"> - <argument name="Block" value="$$createPreReqBlock$$"/> - <argument name="CmsPage" value="$$createCMSPage$$"/> - </actionGroup> - <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> - <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> - </actionGroup> - <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="All Store View" stepKey="selectAllStoreView" /> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE" /> - <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> - <waitForPageLoad stepKey="waitForPageLoad2" /> - <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> - <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> - <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> - <argument name="FolderName" value="Storage Root"/> - </actionGroup> - <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> - <argument name="Image" value="ImageUpload"/> - </actionGroup> - <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> - <argument name="Image" value="ImageUpload"/> - </actionGroup> - <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> - <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> - <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="clickSaveBlock"/> - <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> - <waitForPageLoad stepKey="waitForPageLoad11" /> - <!--see image on Storefront--> - <seeElement selector="{{StorefrontBlockSection.mediaDescription}}" stepKey="assertMediaDescription"/> - <seeElementInDOM selector="{{StorefrontBlockSection.imageSource(ImageUpload.fileName)}}" stepKey="assertMediaSource"/> <after> - <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> - <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnEditPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> - <waitForPageLoad stepKey="waitForGridReload"/> - <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml index d361fb60f31c7..a56d5d0710d3a 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml @@ -7,69 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminAddImageToWYSIWYGCMSTest"> + <test name="AwsS3AdminAddImageToWYSIWYGCMSTest" extends="AdminAddImageToWYSIWYGCMSTest"> <annotations> - <features value="Cms"/> - <stories value="MC-37460: Support by Magento CMS"/> - <group value="Cms"/> - <title value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> - <description value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> + <description value="Admin should be able to add image to WYSIWYG content of CMS Page"/> <severity value="BLOCKER"/> <testCaseId value="MC-38295"/> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> - <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> - <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> <after> - <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> - <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> - - <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> - <argument name="CMSPage" value="$$createCMSPage$$"/> - </actionGroup> - <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> - <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> - <waitForPageLoad stepKey="waitForPageLoad" /> - <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> - <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> - <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> - <argument name="FolderName" value="Storage Root"/> - </actionGroup> - <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> - <argument name="Image" value="ImageUpload3"/> - </actionGroup> - <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> - <argument name="Image" value="ImageUpload3"/> - </actionGroup> - <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> - <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> - <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> - <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="$$createCMSPage.identifier$$" stepKey="fillFieldUrlKey"/> - <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> - <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> - <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> - <see userInput="You saved the page." stepKey="seeSuccessMessage"/> - <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> - <waitForPageLoad stepKey="wait4"/> - <seeElement selector="{{StorefrontCMSPageSection.mediaDescription}}" stepKey="assertMediaDescription"/> - <seeElementInDOM selector="{{StorefrontCMSPageSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml new file mode 100644 index 0000000000000..adc4eea8acf2e --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml @@ -0,0 +1,28 @@ +<?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="AwsS3AdminAddImageToWYSIWYGNewsletterTest" extends="AdminAddImageToWYSIWYGNewsletterTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Apply new WYSIWYG in Newsletter"/> + <group value="Newsletter"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of Newsletter"/> + <description value="Admin should be able to add image to WYSIWYG content Newsletter"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38716"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml new file mode 100644 index 0000000000000..2b46ddcacb94c --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml @@ -0,0 +1,30 @@ +<?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="AwsS3AdminAddRemoveDefaultVideoSimpleProductTest" extends="AdminAddRemoveDefaultVideoSimpleProductTest"> + <annotations> + <title value="AWS S3Admin should be able to add/remove default product video for a Simple Product"/> + <stories value="Add/remove images and videos for all product types and category"/> + <description value="Admin should be able to add/remove default product video for a Simple Product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38693"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MC-33903"/> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml index 449f00281c425..dd0fe36f44dde 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml @@ -20,9 +20,12 @@ <testCaseId value="MC-38039"/> <group value="Downloadable"/> <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MQE-2288" /> + </skip> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -31,7 +34,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> </before> <after> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml index 4d411fcffc682..d9dc75c18ad4b 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml @@ -7,55 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminMarketingCreateSitemapEntityTest"> + <test name="AwsS3AdminMarketingCreateSitemapEntityTest" extends="AdminMarketingCreateSitemapEntityTest"> <annotations> - <features value="Sitemap"/> - <stories value="AWS S3 Admin Creates Sitemap Entity"/> - <title value="AWS S3 Sitemap Creation"/> + <stories value="Admin Creates Sitemap Entity"/> <description value="Sitemap Entity Creation"/> - <testCaseId value="MC-38319"/> <severity value="MAJOR"/> - <group value="sitemap"/> - <group value="mtf_migrated"/> + <title value="AWS S3 Sitemap Creation"/> + <testCaseId value="MC-38319"/> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> <after> - <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteCreatedSitemap"> - <argument name="filename" value="sitemap.xml"/> - </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> - - <!--TEST BODY --> - <!--Navigate to Marketing->Sitemap Page --> - <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSiteMapPage"> - <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> - <argument name="submenuUiId" value="{{AdminMenuSEOAndSearchSiteMap.dataUiId}}"/> - </actionGroup> - <!-- Navigate to New Sitemap Creation Page --> - <actionGroup ref="AdminMarketingNavigateToNewSitemapPageActionGroup" stepKey="navigateToAddNewSitemap"/> - <!-- Create Sitemap Entity --> - <actionGroup ref="AdminMarketingCreateSitemapEntityActionGroup" stepKey="createSitemap"> - <argument name="filename" value="sitemap.xml"/> - <argument name="path" value="/"/> - </actionGroup> - <!-- Assert Success Message --> - <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeSuccessMessage"> - <argument name="message" value="You saved the sitemap."/> - <argument name="messageType" value="success"/> - </actionGroup> - <!-- Find Created Sitemap On Grid --> - <actionGroup ref="AdminMarketingSearchSitemapActionGroup" stepKey="findCreatedSitemapInGrid"> - <argument name="name" value="sitemap.xml"/> - </actionGroup> - <actionGroup ref="AssertAdminSitemapInGridActionGroup" stepKey="assertSitemapInGrid"> - <argument name="name" value="sitemap.xml"/> - </actionGroup> - <!--END TEST BODY --> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml index 43cf305e3fd17..bbdeb7ff1155a 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml @@ -7,33 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminMarketingSiteMapCreateNewTest"> + <test name="AwsS3AdminMarketingSiteMapCreateNewTest" extends="AdminMarketingSiteMapCreateNewTest"> <annotations> - <features value="Sitemap"/> - <stories value="AWS S3 Create Site Map"/> <title value="AWS S3 Create New Site Map with valid data"/> + <stories value="Create Site Map"/> <description value="Create New Site Map with valid data"/> - <testCaseId value="MC-38320" /> <severity value="CRITICAL"/> - <group value="sitemap"/> + <testCaseId value="MC-38320" /> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> <after> - <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteSiteMap"> - <argument name="filename" value="{{DefaultSiteMap.filename}}" /> - </actionGroup> - <actionGroup ref="AssertSiteMapDeleteSuccessActionGroup" stepKey="assertDeleteSuccessMessage"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> - <actionGroup ref="AdminMarketingSiteMapNavigateNewActionGroup" stepKey="navigateNewSiteMap"/> - <actionGroup ref="AdminMarketingSiteMapFillFormActionGroup" stepKey="fillSiteMapForm"> - <argument name="sitemap" value="DefaultSiteMap" /> - </actionGroup> - <actionGroup ref="AssertSiteMapCreateSuccessActionGroup" stepKey="seeSuccessMessage"/> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml new file mode 100644 index 0000000000000..6d9d89fd29be5 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml @@ -0,0 +1,26 @@ +<?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="AwsS3CheckingRMAPrintTest" extends="CheckingRMAPrintTest"> + <annotations> + <title value="AWS S3 Checking Returns Print"/> + <stories value="Exception when try to print RMA"/> + <description value="RMA file should be downloaded"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38694"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml new file mode 100644 index 0000000000000..049caa2180d69 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -0,0 +1,29 @@ +<?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="AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest" extends="ConfigurableProductChildImageShouldBeShownOnWishListTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Configurable product child image should be Shown on wishlist"/> + <group value="wishlist"/> + <title value="AWS S3 when user add Configurable child product to WIshlist then child product image should be shown in Wishlist"/> + <description value="When user add Configurable child product to WIshlist then child product image should be shown in Wishlist"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38708"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml new file mode 100644 index 0000000000000..c8d2947632b59 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml @@ -0,0 +1,30 @@ +<?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="AwsS3StorefrontPrintOrderGuestTest" extends="StorefrontPrintOrderGuestTest"> + <annotations> + <title value="AWS S3 Print Order from Guest on Frontend"/> + <stories value="Print Order"/> + <description value="Print Order from Guest on Frontend"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38689"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MQE-2288" /> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml new file mode 100644 index 0000000000000..8e2ec348d4f41 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml @@ -0,0 +1,27 @@ +<?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="AwsS3UpdateImageFileCustomerAttributeTest" extends="UpdateImageFileCustomerAttributeTest"> + <annotations> + <title value="AWS S3 Update image file customer attribute test"/> + <stories value="Update Customer Custom Attributes"/> + <description value="Update image file customer attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38692"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json index 02733f01d2285..ce5396223f58d 100644 --- a/app/code/Magento/AwsS3/composer.json +++ b/app/code/Magento/AwsS3/composer.json @@ -1,6 +1,9 @@ { "name": "magento/module-aws-s-3", "description": "N/A", + "config": { + "sort-packages": true + }, "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "^100.0.2", diff --git a/app/code/Magento/Catalog/Model/Category/FileInfo.php b/app/code/Magento/Catalog/Model/Category/FileInfo.php index 7d679f2645be1..f5aec60b2fcc0 100644 --- a/app/code/Magento/Catalog/Model/Category/FileInfo.php +++ b/app/code/Magento/Catalog/Model/Category/FileInfo.php @@ -239,7 +239,8 @@ private function getMediaDirectoryPathRelativeToBaseDirectoryPath(string $filePa $mediaDirectoryRelativeSubpath = substr($mediaDirectoryPath, strlen($baseDirectoryPath)); $pubDirectory = $baseDirectory->getRelativePath($pubDirectoryPath); - if (strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 && strpos($filePath, $pubDirectory) !== 0) { + if ($pubDirectory && strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 + && strpos($filePath, $pubDirectory) !== 0) { $mediaDirectoryRelativeSubpath = substr($mediaDirectoryRelativeSubpath, strlen($pubDirectory)); } diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index b8ab4e32ec161..f5546a06dd235 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -67,8 +67,6 @@ <media_storage_configuration> <allowed_resources> <tmp_images_folder>tmp</tmp_images_folder> - <catalog_product_images>media/catalog/product/</catalog_product_images> - <catalog_product_images_tmp>media/tmp/catalog/product/</catalog_product_images_tmp> <catalog_images_folder>catalog</catalog_images_folder> <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 8b170ecdd5c04..2c94e2e76914f 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -363,7 +363,6 @@ public function getFilesCollection($path, $type = null) $collection->setFilesFilter('/\.(' . implode('|', $allowed) . ')$/i'); } - // prepare items foreach ($collection as $item) { $item->setId($this->_cmsWysiwygImages->idEncode($item->getBasename())); $item->setName($item->getBasename()); @@ -383,7 +382,9 @@ public function getFilesCollection($path, $type = null) } try { - $size = getimagesize($item->getFilename()); + $size = getimagesizefromstring( + $driver->fileGetContents($item->getFilename()) + ); if (is_array($size)) { $item->setWidth($size[0]); @@ -657,7 +658,7 @@ public function resizeFile($source, $keepRatio = true) $image->keepAspectRatio($keepRatio); - list($imageWidth, $imageHeight) = $this->getResizedParams($source); + [$imageWidth, $imageHeight] = $this->getResizedParams($source); $image->resize($imageWidth, $imageHeight); $dest = $targetDir . '/' . $this->ioFile->getPathInfo($source)['basename']; @@ -680,7 +681,7 @@ private function getResizedParams(string $source): array $configHeight = $this->_resizeParameters['height']; //phpcs:ignore Generic.PHP.NoSilencedErrors - list($imageWidth, $imageHeight) = @getimagesize($source); + [$imageWidth, $imageHeight] = @getimagesize($source); if ($imageWidth && $imageHeight) { $imageWidth = $configWidth > $imageWidth ? $imageWidth : $configWidth; diff --git a/app/code/Magento/Cms/etc/config.xml b/app/code/Magento/Cms/etc/config.xml index d7a9e172f59a6..c1b3717386454 100644 --- a/app/code/Magento/Cms/etc/config.xml +++ b/app/code/Magento/Cms/etc/config.xml @@ -31,6 +31,7 @@ <media_storage_configuration> <allowed_resources> <wysiwyg_image_folder>wysiwyg</wysiwyg_image_folder> + <preview_folder>.thumbs</preview_folder> </allowed_resources> </media_storage_configuration> </system> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index b4c360c3e0538..48f2aad8fa746 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -10,7 +10,6 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Driver\File; use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; @@ -74,19 +73,36 @@ public function __construct( public function execute(string $path): AssetInterface { $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); - $file = $this->getFileInfo->execute($absolutePath); - [$width, $height] = getimagesize($absolutePath); + $driver = $this->getMediaDirectory()->getDriver(); + + if ($driver instanceof Filesystem\ExtendedDriverInterface) { + $meta = $driver->getMetadata($absolutePath); + } else { + /** + * SPL file info is not compatible with remote storages and must not be used. + */ + $file = $this->getFileInfo->execute($absolutePath); + $meta = [ + 'size' => $file->getSize(), + 'extension' => $file->getExtension(), + 'basename' => $file->getBasename(), + ]; + } + + [$width, $height] = getimagesizefromstring( + $this->getMediaDirectory()->readFile($absolutePath) + ); return $this->assetFactory->create( [ 'id' => null, 'path' => $path, - 'title' => $file->getBasename(), + 'title' => $meta['basename'], 'width' => $width, 'height' => $height, 'hash' => $this->getHash($path), - 'size' => $file->getSize(), - 'contentType' => 'image/' . $file->getExtension(), + 'size' => $meta['size'], + 'contentType' => 'image/' . $meta['extension'], 'source' => 'Local' ] ); @@ -105,12 +121,12 @@ private function getHash(string $path): string } /** - * Retrieve media directory instance with read access + * Retrieve media directory instance with write access * - * @return ReadInterface + * @return Filesystem\Directory\WriteInterface */ - private function getMediaDirectory(): ReadInterface + private function getMediaDirectory(): Filesystem\Directory\WriteInterface { - return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } } diff --git a/app/code/Magento/MediaStorage/App/Media.php b/app/code/Magento/MediaStorage/App/Media.php index ca5ff458c52e9..f3a85cf3a9baa 100644 --- a/app/code/Magento/MediaStorage/App/Media.php +++ b/app/code/Magento/MediaStorage/App/Media.php @@ -11,6 +11,7 @@ use Closure; use Exception; use LogicException; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App; use Magento\Framework\App\Area; @@ -18,6 +19,7 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\State; use Magento\Framework\AppInterface; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Driver\File; @@ -103,6 +105,11 @@ class Media implements AppInterface */ private $imageResize; + /** + * @var string + */ + private $mediaUrlFormat; + /** * @param ConfigFactory $configFactory * @param SynchronizationFactory $syncFactory @@ -116,6 +123,8 @@ class Media implements AppInterface * @param State $state * @param ImageResize $imageResize * @param File $file + * @param CatalogMediaConfig $catalogMediaConfig + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -130,12 +139,19 @@ public function __construct( PlaceholderFactory $placeholderFactory, State $state, ImageResize $imageResize, - File $file + File $file, + CatalogMediaConfig $catalogMediaConfig = null ) { $this->response = $response; $this->isAllowed = $isAllowed; - $this->directoryPub = $filesystem->getDirectoryWrite(DirectoryList::PUB); - $this->directoryMedia = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->directoryPub = $filesystem->getDirectoryWrite( + DirectoryList::PUB, + Filesystem\DriverPool::FILE + ); + $this->directoryMedia = $filesystem->getDirectoryWrite( + DirectoryList::MEDIA, + Filesystem\DriverPool::FILE + ); $mediaDirectory = trim($mediaDirectory); if (!empty($mediaDirectory)) { // phpcs:ignore Magento2.Functions.DiscouragedFunction @@ -148,6 +164,9 @@ public function __construct( $this->placeholderFactory = $placeholderFactory; $this->appState = $state; $this->imageResize = $imageResize; + + $catalogMediaConfig = $catalogMediaConfig ?: App\ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaUrlFormat = $catalogMediaConfig->getMediaUrlFormat(); } /** @@ -174,10 +193,8 @@ public function launch(): ResponseInterface } try { - /** @var Synchronization $sync */ - $sync = $this->syncFactory->create(['directory' => $this->directoryPub]); - $sync->synchronize($this->relativeFileName); - $this->imageResize->resizeFromImageName($this->getOriginalImage($this->relativeFileName)); + $this->createLocalCopy(); + if ($this->directoryPub->isReadable($this->relativeFileName)) { $this->response->setFilePath($this->directoryPub->getAbsolutePath($this->relativeFileName)); } else { @@ -190,6 +207,25 @@ public function launch(): ResponseInterface return $this->response; } + /** + * Create local copy of file and perform resizing if necessary. + * + * @throws NotFoundException + */ + private function createLocalCopy(): void + { + $this->syncFactory->create(['directory' => $this->directoryPub]) + ->synchronize($this->relativeFileName); + + if ($this->directoryPub->isReadable($this->relativeFileName)) { + return; + } + + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $this->imageResize->resizeFromImageName($this->getOriginalImage($this->relativeFileName)); + } + } + /** * Check if media directory changed * diff --git a/app/code/Magento/MediaStorage/Model/File/Storage.php b/app/code/Magento/MediaStorage/Model/File/Storage.php index 861f2d82c7e7b..f93b9180fa23d 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage.php @@ -83,11 +83,9 @@ class Storage extends AbstractModel protected $_databaseFactory; /** - * Filesystem instance - * - * @var Filesystem + * @var Filesystem\Directory\ReadInterface */ - protected $filesystem; + private $localMediaDirectory; /** * @param \Magento\Framework\Model\Context $context @@ -124,7 +122,10 @@ public function __construct( $this->_fileFlag = $fileFlag; $this->_fileFactory = $fileFactory; $this->_databaseFactory = $databaseFactory; - $this->filesystem = $filesystem; + $this->localMediaDirectory = $filesystem->getDirectoryRead( + DirectoryList::MEDIA, + Filesystem\DriverPool::FILE + ); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -286,7 +287,7 @@ public function synchronize($storage) public function getScriptConfig() { $config = []; - $config['media_directory'] = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath(); + $config['media_directory'] = $this->localMediaDirectory->getAbsolutePath(); $allowedResources = $this->_coreConfig->getValue(self::XML_PATH_MEDIA_RESOURCE_WHITELIST, 'default'); foreach ($allowedResources as $allowedResource) { diff --git a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php index 7f70f5ba48e5c..068732a7225cd 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php @@ -10,21 +10,23 @@ use Exception; use LogicException; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\View\Asset\Placeholder; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Read; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\MediaStorage\App\Media; use Magento\MediaStorage\Model\File\Storage\Config; use Magento\MediaStorage\Model\File\Storage\ConfigFactory; use Magento\MediaStorage\Model\File\Storage\Response; use Magento\MediaStorage\Model\File\Storage\Synchronization; use Magento\MediaStorage\Model\File\Storage\SynchronizationFactory; +use Magento\MediaStorage\Service\ImageResize; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -80,122 +82,106 @@ class MediaTest extends TestCase private $directoryMediaMock; /** - * @var \Magento\Framework\Filesystem\Directory\Read|MockObject + * @var Read|MockObject */ private $directoryPubMock; + /** + * @inheritDoc + */ protected function setUp(): void { $this->configMock = $this->createMock(Config::class); $this->sync = $this->createMock(Synchronization::class); - $this->configFactoryMock = $this->createPartialMock( - ConfigFactory::class, - ['create'] - ); - $this->configFactoryMock->expects($this->any()) - ->method('create') + $this->configFactoryMock = $this->createPartialMock(ConfigFactory::class, ['create']); + $this->responseMock = $this->createMock(Response::class); + $this->syncFactoryMock = $this->createPartialMock(SynchronizationFactory::class, ['create']); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->directoryPubMock = $this->getMockForAbstractClass(WriteInterface::class); + $this->directoryMediaMock = $this->getMockForAbstractClass(WriteInterface::class); + + $this->configFactoryMock->method('create') ->willReturn($this->configMock); - $this->syncFactoryMock = $this->createPartialMock( - SynchronizationFactory::class, - ['create'] - ); - $this->syncFactoryMock->expects($this->any()) - ->method('create') + $this->syncFactoryMock->method('create') ->willReturn($this->sync); - - $this->filesystemMock = $this->createMock(Filesystem::class); - $this->directoryPubMock = $this->getMockForAbstractClass( - WriteInterface::class, - [], - '', - false, - true, - true, - ['isReadable', 'getAbsolutePath'] - ); - $this->directoryMediaMock = $this->getMockForAbstractClass( - WriteInterface::class, - [], - '', - false, - true, - true, - ['getAbsolutePath'] - ); - $this->filesystemMock->expects($this->any()) - ->method('getDirectoryWrite') + $this->filesystemMock->method('getDirectoryWrite') ->willReturnMap([ [DirectoryList::PUB, DriverPool::FILE, $this->directoryPubMock], [DirectoryList::MEDIA, DriverPool::FILE, $this->directoryMediaMock], ]); - - $this->responseMock = $this->createMock(Response::class); } - protected function tearDown(): void + public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided(): void { - unset($this->mediaModel); - } - - public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided() - { - $this->mediaModel = $this->getMediaModel(); - $filePath = '/absolute/path/to/test/file.png'; - $this->directoryMediaMock->expects($this->once()) + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::once()) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); - $this->configMock->expects($this->once())->method('save'); - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryPubMock->expects($this->once()) + $this->configMock->expects(self::once()) + ->method('save'); + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->responseMock->expects($this->once())->method('setFilePath')->with($filePath); - $this->mediaModel->launch(); + $this->responseMock->expects(self::once()) + ->method('setFilePath') + ->with($filePath); + + $this->createMediaModel()->launch(); } - public function testProcessRequestReturnsFileIfItsProperlySynchronized() + public function testProcessRequestReturnsFileIfItsProperlySynchronized(): void { - $this->mediaModel = $this->getMediaModel(); + $this->mediaModel = $this->createMediaModel(); $filePath = '/absolute/path/to/test/file.png'; - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryMediaMock->expects($this->once()) + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::once()) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); - $this->responseMock->expects($this->once())->method('setFilePath')->with($filePath); - $this->assertSame($this->responseMock, $this->mediaModel->launch()); + $this->responseMock->expects(self::once()) + ->method('setFilePath') + ->with($filePath); + + self::assertSame($this->responseMock, $this->mediaModel->launch()); } - public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized() + public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized(): void { - $this->mediaModel = $this->getMediaModel(); + $this->mediaModel = $this->createMediaModel(); - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryMediaMock->expects($this->once()) + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(false); - $this->assertSame($this->responseMock, $this->mediaModel->launch()); + + self::assertSame($this->responseMock, $this->mediaModel->launch()); } /** @@ -204,7 +190,7 @@ public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized() * * @dataProvider catchExceptionDataProvider */ - public function testCatchException($isDeveloper, $setBodyCalls) + public function testCatchException(bool $isDeveloper, int $setBodyCalls): void { /** @var Bootstrap|MockObject $bootstrap */ $bootstrap = $this->createMock(Bootstrap::class); @@ -212,41 +198,39 @@ public function testCatchException($isDeveloper, $setBodyCalls) /** @var Exception|MockObject $exception */ $exception = $this->createMock(Exception::class); - $this->responseMock->expects($this->once()) + $this->responseMock->expects(self::once()) ->method('setHttpResponseCode') ->with(404); - $bootstrap->expects($this->once()) + $bootstrap->expects(self::once()) ->method('isDeveloperMode') ->willReturn($isDeveloper); - $this->responseMock->expects($this->exactly($setBodyCalls)) + $this->responseMock->expects(self::exactly($setBodyCalls)) ->method('setBody'); - $this->responseMock->expects($this->once()) + $this->responseMock->expects(self::once()) ->method('sendResponse'); - $this->mediaModel = $this->getMediaModel(); - - $this->mediaModel->catchException($bootstrap, $exception); + $this->createMediaModel()->catchException($bootstrap, $exception); } - public function testExceptionWhenIsAllowedReturnsFalse() + public function testExceptionWhenIsAllowedReturnsFalse(): void { - $this->mediaModel = $this->getMediaModel(false); - $this->directoryMediaMock->expects($this->once()) + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->configMock->expects($this->once())->method('save'); + $this->configMock->expects(self::once()) + ->method('save'); $this->expectException(LogicException::class); $this->expectExceptionMessage('The path is not allowed: ' . self::RELATIVE_FILE_PATH); - $this->mediaModel->launch(); + $this->createMediaModel(false)->launch(); } /** * @return array */ - public function catchExceptionDataProvider() + public function catchExceptionDataProvider(): array { return [ 'default mode' => [false, 0], @@ -260,35 +244,30 @@ public function catchExceptionDataProvider() * @param bool $isAllowed * @return Media */ - protected function getMediaModel(bool $isAllowed = true): Media + protected function createMediaModel(bool $isAllowed = true): Media { - $objectManager = new ObjectManager($this); - $isAllowedCallback = function () use ($isAllowed) { return $isAllowed; }; - /** @var Media $mediaClass */ - $mediaClass = $objectManager->getObject( - Media::class, - [ - 'configFactory' => $this->configFactoryMock, - 'syncFactory' => $this->syncFactoryMock, - 'response' => $this->responseMock, - 'isAllowed' => $isAllowedCallback, - 'mediaDirectory' => false, - 'configCacheFile' => self::CACHE_FILE_PATH, - 'relativeFileName' => self::RELATIVE_FILE_PATH, - 'filesystem' => $this->filesystemMock, - 'placeholderFactory' => $this->createConfiguredMock( - PlaceholderFactory::class, - [ - 'create' => $this->createMock(Placeholder::class) - ] - ), - ] - ); + $placeholderFactory = $this->createMock(PlaceholderFactory::class); + $placeholderFactory->method('create') + ->willReturn($this->createMock(Placeholder::class)); - return $mediaClass; + return new Media( + $this->configFactoryMock, + $this->syncFactoryMock, + $this->responseMock, + $isAllowedCallback, + false, + self::CACHE_FILE_PATH, + self::RELATIVE_FILE_PATH, + $this->filesystemMock, + $placeholderFactory, + $this->createMock(State::class), + $this->createMock(ImageResize::class), + $this->createMock(Filesystem\Driver\File::class), + $this->createMock(CatalogMediaConfig::class) + ); } } diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php index e308f609a3d69..bc21700cadee0 100644 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php @@ -28,8 +28,8 @@ class RemoteStorageEnableCommand extends Command private const ARG_DRIVER = 'driver'; private const ARGUMENT_BUCKET = 'bucket'; private const ARGUMENT_REGION = 'region'; - private const ARGUMENT_ACCESS_KEY = 'access-key'; - private const ARGUMENT_SECRET_KEY = 'secret-key'; + private const OPTION_ACCESS_KEY = 'access-key'; + private const OPTION_SECRET_KEY = 'secret-key'; private const ARGUMENT_PREFIX = 'prefix'; private const OPTION_IS_PUBLIC = 'is-public'; @@ -66,8 +66,8 @@ protected function configure(): void ->addArgument(self::ARGUMENT_BUCKET, InputArgument::OPTIONAL, 'Bucket') ->addArgument(self::ARGUMENT_REGION, InputArgument::OPTIONAL, 'Region') ->addArgument(self::ARGUMENT_PREFIX, InputArgument::OPTIONAL, 'Prefix', '') - ->addArgument(self::ARGUMENT_ACCESS_KEY, InputArgument::OPTIONAL, 'Access key') - ->addArgument(self::ARGUMENT_SECRET_KEY, InputArgument::OPTIONAL, 'Secret key') + ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_OPTIONAL, 'Access key') + ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_OPTIONAL, 'Secret key') ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_REQUIRED, 'Is public', false); } @@ -105,8 +105,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]; $isPublic = (bool)$input->getOption(self::OPTION_IS_PUBLIC); - if (($key = (string)$input->getArgument(self::ARGUMENT_ACCESS_KEY)) - && ($secret = (string)$input->getArgument(self::ARGUMENT_SECRET_KEY)) + if (($key = (string)$input->getOption(self::OPTION_ACCESS_KEY)) + && ($secret = (string)$input->getOption(self::OPTION_SECRET_KEY)) ) { $config['credentials']['key'] = $key; $config['credentials']['secret'] = $secret; diff --git a/app/code/Magento/RemoteStorage/Driver/DriverException.php b/app/code/Magento/RemoteStorage/Driver/DriverException.php new file mode 100644 index 0000000000000..b35a7e7c4d4da --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Remote storage driver. + */ +class DriverException extends LocalizedException +{ +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php index ab7a1bcaa6cc5..5268cbaea4a77 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -7,8 +7,6 @@ namespace Magento\RemoteStorage\Driver; -use Magento\Framework\Filesystem\DriverInterface; - /** * Factory for drivers with additional configuration. */ @@ -19,7 +17,7 @@ interface DriverFactoryInterface * * @param array $config * @param string $prefix - * @return DriverInterface + * @return RemoteDriverInterface */ - public function create(array $config, string $prefix): DriverInterface; + public function create(array $config, string $prefix): RemoteDriverInterface; } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php index aa4e057af5383..d13f599387d90 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php @@ -22,7 +22,7 @@ class DriverFactoryPool /** * @param DriverFactoryInterface[] $pool */ - public function __construct(array $pool) + public function __construct(array $pool = []) { $this->pool = $pool; } @@ -49,7 +49,7 @@ public function has(string $name): bool public function get(string $name): DriverFactoryInterface { if (!$this->has($name)) { - throw new RuntimeException(__('Factory %1 does not exist', $name)); + throw new RuntimeException(__('Driver "%1" does not exist', $name)); } return $this->pool[$name]; diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index a1e758f170e7e..731ec6686e657 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -20,7 +20,7 @@ class DriverPool implements DriverPoolInterface { public const PATH_DRIVER = 'remote_storage/driver'; - public const PATH_IS_PUBLIC = 'remote_storage/is_public'; + public const PATH_EXPOSE_URLS = 'remote_storage/expose_urls'; public const PATH_PREFIX = 'remote_storage/prefix'; public const PATH_CONFIG = 'remote_storage/config'; @@ -68,7 +68,7 @@ public function __construct( * Retrieves remote driver. * * @param string $code - * @return DriverInterface + * @return RemoteDriverInterface * @throws RuntimeException * @throws FileSystemException */ diff --git a/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php b/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php new file mode 100644 index 0000000000000..fc108bb388cb5 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Filesystem\ExtendedDriverInterface; + +/** + * Remote storage driver. + */ +interface RemoteDriverInterface extends ExtendedDriverInterface +{ + /** + * Test storage connection. + * + * @throws DriverException + */ + public function test(): void; +} diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php index 36238b20b38bb..41fbfdde15bd0 100644 --- a/app/code/Magento/RemoteStorage/Model/Config.php +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -34,13 +34,13 @@ public function __construct(DeploymentConfig $config) /** * Retrieve driver name. * - * @return string|null + * @return string * @throws FileSystemException * @throws RuntimeException */ - public function getDriver(): ?string + public function getDriver(): string { - return $this->config->get(DriverPool::PATH_DRIVER, null); + return $this->config->get(DriverPool::PATH_DRIVER, BaseDriverPool::FILE); } /** @@ -52,23 +52,11 @@ public function getDriver(): ?string */ public function isEnabled(): bool { - $driver = $this->config->get(DriverPool::PATH_DRIVER); + $driver = $this->getDriver(); return $driver && $driver !== BaseDriverPool::FILE; } - /** - * Use remote URL for public URLs. - * - * @return bool - * @throws FileSystemException - * @throws RuntimeException - */ - public function isPublic(): bool - { - return (bool)$this->config->get(DriverPool::PATH_IS_PUBLIC, false); - } - /** * Retrieves config. * @@ -85,6 +73,7 @@ public function getConfig(): array * Retrieves prefix. * * @return string + * * @throws FileSystemException * @throws RuntimeException */ @@ -92,4 +81,16 @@ public function getPrefix(): string { return (string)$this->config->get(DriverPool::PATH_PREFIX, ''); } + + /** + * Retrieves value for exposing URLs. + * + * @return bool + * @throws FileSystemException + * @throws RuntimeException + */ + public function getExposeUrls(): bool + { + return (bool)$this->config->get(DriverPool::PATH_EXPOSE_URLS, false); + } } diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php index 66c5fe1a5ac67..013e3fd23e168 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Image.php +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -182,7 +182,7 @@ public function __destruct() * @return string * @throws FileSystemException */ - private function copyFileToTmp($filePath): string + private function copyFileToTmp(string $filePath): string { if ($this->fileExistsInTmp($filePath)) { return $filePath; diff --git a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php index 59e21a9c237d0..12837545c533b 100644 --- a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php +++ b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php @@ -23,6 +23,11 @@ */ class MediaStorage { + /** + * @var Filesystem + */ + private $filesystem; + /** * @var bool */ @@ -31,12 +36,12 @@ class MediaStorage /** * @var WriteInterface */ - private $remoteDir; + private $remoteDirectory; /** * @var WriteInterface */ - private $localDir; + private $localDirectory; /** * @param Config $config @@ -47,12 +52,13 @@ class MediaStorage public function __construct(Config $config, Filesystem $filesystem) { $this->isEnabled = $config->isEnabled(); - $this->remoteDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); - $this->localDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); + $this->remoteDirectory = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); + $this->localDirectory = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); } /** * Download remote file + * * @param Synchronization $subject * @param string $relativeFileName * @return null @@ -60,21 +66,18 @@ public function __construct(Config $config, Filesystem $filesystem) * @throws ValidatorException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSynchronize(Synchronization $subject, string $relativeFileName) + public function beforeSynchronize(Synchronization $subject, string $relativeFileName): void { - if ($this->isEnabled) { - if ($this->remoteDir->isExist($relativeFileName)) { - $file = $this->localDir->openFile($relativeFileName, 'w'); - try { - $file->lock(); - $file->write($this->remoteDir->readFile($relativeFileName)); - $file->unlock(); - $file->close(); - } catch (FileSystemException $e) { - $file->close(); - } + if ($this->isEnabled && $this->remoteDirectory->isExist($relativeFileName)) { + $file = $this->localDirectory->openFile($relativeFileName, 'w'); + try { + $file->lock(); + $file->write($this->remoteDirectory->readFile($relativeFileName)); + $file->unlock(); + $file->close(); + } catch (FileSystemException $e) { + $file->close(); } } - return null; } } diff --git a/app/code/Magento/RemoteStorage/Plugin/Scope.php b/app/code/Magento/RemoteStorage/Plugin/Scope.php index ab723fa1d0c19..6a05b63dee3a6 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Scope.php +++ b/app/code/Magento/RemoteStorage/Plugin/Scope.php @@ -36,7 +36,7 @@ class Scope */ public function __construct(Config $config, Filesystem $filesystem) { - $this->isEnabled = $config->isEnabled() && $config->isPublic(); + $this->isEnabled = $config->isEnabled() && $config->getExposeUrls(); $this->filesystem = $filesystem; } diff --git a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php deleted file mode 100644 index 2e93949b40fce..0000000000000 --- a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Plugin; - -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\RemoteStorage\Driver\DriverPool; -use Magento\RemoteStorage\Filesystem; -use Magento\RemoteStorage\Model\Config; -use Magento\Sitemap\Model\Sitemap as BaseSitemap; - -/** - * Plugin to replace file URL with remote URL. - */ -class Sitemap -{ - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var bool - */ - private $isEnabled; - - /** - * @param Filesystem $filesystem - * @param Config $config - */ - public function __construct(Filesystem $filesystem, Config $config) - { - $this->filesystem = $filesystem; - $this->isEnabled = $config->isEnabled(); - } - - /** - * Modifies image URl to point to correct remote storage. - * - * @param BaseSitemap $subject - * @param string $result - * @param string $sitemapPath - * @param string $sitemapFileName - * @return string - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetSitemapUrl( - BaseSitemap $subject, - string $result, - string $sitemapPath, - string $sitemapFileName - ): string { - if ($this->isEnabled) { - $path = trim($sitemapPath . $sitemapFileName, '/'); - - return $this->filesystem->getDirectoryRead(DirectoryList::ROOT, DriverPool::REMOTE) - ->getAbsolutePath($path); - } - - return $result; - } -} diff --git a/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php b/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..b625661479962 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php @@ -0,0 +1,201 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverFactoryPool; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\TextConfigOption; +use Psr\Log\LoggerInterface; + +/** + * Remote storage options. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + private const OPTION_REMOTE_STORAGE_DRIVER = 'remote-storage-driver'; + private const CONFIG_PATH__REMOTE_STORAGE_DRIVER = RemoteDriverPool::PATH_DRIVER; + private const OPTION_REMOTE_STORAGE_PREFIX = 'remote-storage-prefix'; + private const CONFIG_PATH__REMOTE_STORAGE_PREFIX = RemoteDriverPool::PATH_PREFIX; + private const OPTION_REMOTE_STORAGE_BUCKET = 'remote-storage-bucket'; + private const CONFIG_PATH__REMOTE_STORAGE_BUCKET = RemoteDriverPool::PATH_CONFIG . '/bucket'; + private const OPTION_REMOTE_STORAGE_REGION = 'remote-storage-region'; + private const CONFIG_PATH__REMOTE_STORAGE_REGION = RemoteDriverPool::PATH_CONFIG . '/region'; + private const OPTION_REMOTE_STORAGE_ACCESS_KEY = 'remote-storage-key'; + private const CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY = RemoteDriverPool::PATH_CONFIG . '/credentials/key'; + private const OPTION_REMOTE_STORAGE_SECRET_KEY = 'remote-storage-secret'; + private const CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY = RemoteDriverPool::PATH_CONFIG . '/credentials/secret'; + + /** + * Map of option to config path relations. + * + * @var string[] + */ + private static $map = [ + self::OPTION_REMOTE_STORAGE_PREFIX => self::CONFIG_PATH__REMOTE_STORAGE_PREFIX, + self::OPTION_REMOTE_STORAGE_BUCKET => self::CONFIG_PATH__REMOTE_STORAGE_BUCKET, + self::OPTION_REMOTE_STORAGE_REGION => self::CONFIG_PATH__REMOTE_STORAGE_REGION, + self::OPTION_REMOTE_STORAGE_ACCESS_KEY => self::CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY, + self::OPTION_REMOTE_STORAGE_SECRET_KEY => self::CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY + ]; + + /** + * @var DriverFactoryPool + */ + private $driverFactoryPool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param DriverFactoryPool $driverFactoryPool + * @param LoggerInterface $logger + */ + public function __construct(DriverFactoryPool $driverFactoryPool, LoggerInterface $logger) + { + $this->driverFactoryPool = $driverFactoryPool; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return [ + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_DRIVER, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, + 'Remote storage driver', + DriverPool::FILE + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_PREFIX, + 'Remote storage prefix', + '' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_BUCKET, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_BUCKET, + 'Remote storage bucket' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_REGION, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_REGION, + 'Remote storage region' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_ACCESS_KEY, + TextConfigOption::FRONTEND_WIZARD_PASSWORD, + self::CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY, + 'Remote storage access key', + '' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_SECRET_KEY, + TextConfigOption::FRONTEND_WIZARD_PASSWORD, + self::CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY, + 'Remote storage secret key', + '' + ) + ]; + } + + /** + * @inheritDoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $driver = $options[self::OPTION_REMOTE_STORAGE_DRIVER] ?? DriverPool::FILE; + + if ($driver === DriverPool::FILE) { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $configData->set(self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, $driver); + } else { + $configData = $this->createConfigData($driver, $options); + } + + return [$configData]; + } + + /** + * @inheritDoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig): array + { + $driver = $options[self::OPTION_REMOTE_STORAGE_DRIVER] ?? DriverPool::FILE; + + if ($driver === DriverPool::FILE) { + return []; + } + + $errors = []; + + if (empty($options[self::OPTION_REMOTE_STORAGE_REGION])) { + $errors[] = 'Region is required'; + } + + if (empty($options[self::OPTION_REMOTE_STORAGE_BUCKET])) { + $errors[] = 'Bucket is required'; + } + + if (!$errors) { + $configData = $this->createConfigData($driver, $options); + + try { + $this->driverFactoryPool->get($driver)->create( + $configData->getData()['remote_storage']['config'], + $options[self::OPTION_REMOTE_STORAGE_PREFIX] + )->test(); + } catch (LocalizedException $exception) { + $message = $exception->getMessage(); + + $this->logger->critical($message); + + $errors[] = 'Adapter error: ' . $message; + } + } + + return $errors; + } + + /** + * Creates pre-configured config data object. + * + * @param string $driver + * @param array $options + * @return ConfigData + */ + private function createConfigData(string $driver, array $options): ConfigData + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $configData->set(self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, $driver); + + foreach (self::$map as $option => $configPath) { + if (!empty($options[$option])) { + $configData->set($configPath, $options[$option]); + } + } + + return $configData; + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index fa27e821c817c..7345048a159e3 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -10,6 +10,7 @@ "magento/module-sitemap": "*", "magento/module-cms": "*", "magento/module-downloadable": "*", + "magento/module-catalog": "*", "magento/module-media-storage": "*", "magento/module-import-export": "*", "magento/module-catalog-import-export": "*", diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..5009a05d8b602 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/system.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_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_storage_configuration"> + <field id="media_storage"> + <comment><![CDATA[<strong style="color:red">Warning!</strong> Database media storage will be ignored if remote storage is enabled.]]></comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index bb253bb5d18f7..9684a3ac49dbc 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -43,7 +43,6 @@ <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> - <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> </type> <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Save"> <arguments> @@ -60,9 +59,6 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> - <type name="Magento\Framework\Url\ScopeInterface"> - <plugin name="remote_url" type="Magento\RemoteStorage\Plugin\Scope" /> - </type> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> @@ -77,7 +73,7 @@ </arguments> </type> <type name="Magento\MediaStorage\Model\File\Storage\Synchronization"> - <plugin name="remote_media" type="Magento\RemoteStorage\Plugin\MediaStorage" /> + <plugin name="remoteMedia" type="Magento\RemoteStorage\Plugin\MediaStorage" /> </type> <type name="Magento\Framework\Data\Collection\Filesystem"> <arguments> @@ -97,6 +93,14 @@ <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> </type> + <type name="Magento\Catalog\Model\Category\FileInfo"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Url\ScopeInterface"> + <plugin name="remoteUrl" type="Magento\RemoteStorage\Plugin\Scope"/> + </type> <type name="Magento\ImportExport\Model\Import"> <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> diff --git a/app/code/Magento/Sitemap/etc/config.xml b/app/code/Magento/Sitemap/etc/config.xml index 36b2cc2207422..614421b9dd752 100644 --- a/app/code/Magento/Sitemap/etc/config.xml +++ b/app/code/Magento/Sitemap/etc/config.xml @@ -57,5 +57,12 @@ </jobs> </default> </crontab> + <system> + <media_storage_configuration> + <allowed_resources> + <sitemap_folder>sitemap</sitemap_folder> + </allowed_resources> + </media_storage_configuration> + </system> </default> </config> diff --git a/app/code/Magento/Sitemap/etc/di.xml b/app/code/Magento/Sitemap/etc/di.xml index 4c4a5f98f737a..4771da2f11144 100644 --- a/app/code/Magento/Sitemap/etc/di.xml +++ b/app/code/Magento/Sitemap/etc/di.xml @@ -52,4 +52,16 @@ <argument name="configReader" xsi:type="object">Magento\Sitemap\Model\ItemProvider\CmsPageConfigReader</argument> </arguments> </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="dirs" xsi:type="array"> + <item name="exclude" xsi:type="array"> + <item name="sitemap" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">media[/\\]+sitemap[/\\]*$</item> + </item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index 3ef113fa63fa7..143889364781f 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -160,7 +160,7 @@ public function afterLoad() 'size' => is_array($stat) ? $stat['size'] : 0, //phpcs:ignore Magento2.Functions.DiscouragedFunction 'name' => basename($value), - 'type' => $stat['mimetype'] ?? $this->getMimeType($fileName), + 'type' => $this->getMimeType($fileName), 'exists' => true, ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html index ade4f52d5153f..518926ed52d69 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html @@ -1,3 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php index 363a48d822ace..d20604e27e5c9 100644 --- a/lib/internal/Magento/Framework/Config/DocumentRoot.php +++ b/lib/internal/Magento/Framework/Config/DocumentRoot.php @@ -10,7 +10,7 @@ /** * Document root detector. - * @deprecared Magento always uses the pub directory + * @deprecated Magento always uses the pub directory * @api */ class DocumentRoot diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index be00cb9f64c18..9b16687620e8f 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -131,11 +131,6 @@ class Filesystem extends \Magento\Framework\Data\Collection */ protected $_collectedFiles = []; - /** - * @var \Magento\Framework\Filesystem - */ - private $filesystem; - /** * @var WriteInterface */ @@ -150,7 +145,8 @@ public function __construct( \Magento\Framework\Filesystem $filesystem = null ) { $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); - $this->filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); + + $filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); $this->rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); parent::__construct($this->_entityFactory); } diff --git a/lib/internal/Magento/Framework/File/Mime.php b/lib/internal/Magento/Framework/File/Mime.php index fe23969f32ce3..d61f5054990e8 100644 --- a/lib/internal/Magento/Framework/File/Mime.php +++ b/lib/internal/Magento/Framework/File/Mime.php @@ -7,10 +7,15 @@ namespace Magento\Framework\File; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; /** * Utility for mime type retrieval + * + * @deprecated + * @see Filesystem\ExtendedDriverInterface::getMetadata() */ class Mime { @@ -18,79 +23,52 @@ class Mime * Mime types * * @var array + * + * @deprecated */ protected $mimeTypes = [ - 'txt' => 'text/plain', - 'htm' => 'text/html', + 'txt' => 'text/plain', + 'htm' => 'text/html', 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', 'json' => 'application/json', - 'xml' => 'application/xml', - 'swf' => 'application/x-shockwave-flash', - 'flv' => 'video/x-flv', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', // images - 'png' => 'image/png', - 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'bmp' => 'image/bmp', - 'ico' => 'image/vnd.microsoft.icon', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', // archives - 'zip' => 'application/zip', - 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', - 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', // audio/video - 'mp3' => 'audio/mpeg', - 'qt' => 'video/quicktime', - 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', // adobe - 'pdf' => 'application/pdf', - 'psd' => 'image/vnd.adobe.photoshop', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - ]; - - /** - * List of mime types that can be defined by file extension. - * - * @var array - */ - private $defineByExtensionList = [ - 'txt' => 'text/plain', - 'htm' => 'text/html', - 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', - 'json' => 'application/json', - 'xml' => 'application/xml', - 'svg' => 'image/svg+xml', - ]; - - /** - * List of generic MIME types - * - * The file mime type should be detected by the file's extension if the native mime type is one of the listed below. - * - * @var array - */ - private $genericMimeTypes = [ - 'application/x-empty', - 'inode/x-empty', + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', ]; /** @@ -99,81 +77,48 @@ class Mime private $filesystem; /** - * Mime constructor. - * @param Filesystem $filesystem + * @param Filesystem|null $filesystem */ public function __construct(Filesystem $filesystem = null) { - $this->filesystem = $filesystem; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** - * Get mime type of a file + * Get mime type of a file. * * @param string $file * @return string - * @throws \InvalidArgumentException + * @throws FileSystemException + * + * @deprecated */ public function getMimeType($file) { - $directoryRead = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); - $fileExistsLocally = file_exists($file); - if (!$fileExistsLocally && !$directoryRead->isExist($file)) { - throw new \InvalidArgumentException("File '$file' doesn't exist"); + $driver = $this->filesystem->getDirectoryWrite( + DirectoryList::ROOT, + Filesystem\DriverPool::FILE + )->getDriver(); + + /** + * Try with non-local driver. + */ + if (!$driver->isExists($file)) { + $driver = $this->filesystem->getDirectoryWrite( + DirectoryList::ROOT + )->getDriver(); } - $result = null; - $extension = $this->getFileExtension($file); - - if (function_exists('mime_content_type') && $fileExistsLocally) { - $result = $this->getNativeMimeType($file); - } else { - $imageInfo = getimagesize($file); - $result = $imageInfo['mime']; + if (!$driver->isExists($file)) { + throw new FileSystemException(__("File '$file' doesn't exist")); } - if (null === $result && isset($this->mimeTypes[$extension])) { - $result = $this->mimeTypes[$extension]; - } elseif (null === $result) { - $result = 'application/octet-stream'; + if ($driver instanceof Filesystem\ExtendedDriverInterface) { + return $driver->getMetadata($file)['mimetype']; } - return $result; - } + $mime = new Filesystem\Driver\File\Mime(); - /** - * Get mime type by the native mime_content_type function. - * - * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' - * - * @param string $file - * @return string - */ - private function getNativeMimeType(string $file): string - { - $extension = $this->getFileExtension($file); - $result = mime_content_type($file); - if (isset($this->mimeTypes[$extension], $this->defineByExtensionList[$extension]) - && ( - strpos($result, 'text/') === 0 - || strpos($result, 'image/svg') === 0 - || in_array($result, $this->genericMimeTypes, true) - ) - ) { - $result = $this->mimeTypes[$extension]; - } - - return $result; - } - - /** - * Get file extension by file name. - * - * @param string $file - * @return string - */ - private function getFileExtension(string $file): string - { - return strtolower(pathinfo($file, PATHINFO_EXTENSION)); + return $mime->getMimeType($file); } } diff --git a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php index ff70f0fb9b0c9..db42e03363236 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php @@ -7,15 +7,16 @@ namespace Magento\Framework\File\Test\Unit; -use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test mime type utility for correct + * Test mime type utility for correct. + * + * @deprecated */ class MimeTest extends TestCase { @@ -25,31 +26,64 @@ class MimeTest extends TestCase private $object; /** - * @var ReadInterface|MockObject + * @var Filesystem\DriverInterface|MockObject */ - private $readInterface; + private $localDriverMock; + + /** + * @var Filesystem\DriverInterface|MockObject + */ + private $remoteDriverMock; + + /** + * @var Filesystem|MockObject + */ + private $filesystemMock; + + /** + * @var Filesystem\Directory\WriteInterface|MockObject + */ + private $localDirectoryMock; + + /** + * @var Filesystem\Directory\WriteInterface|MockObject + */ + private $remoteDirectoryMock; /** * @inheritDoc */ protected function setUp(): void { - $this->readInterface = $this->getMockBuilder(ReadInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->localDriverMock = $this->getMockForAbstractClass(Filesystem\DriverInterface::class); + $this->remoteDriverMock = $this->getMockForAbstractClass(Filesystem\ExtendedDriverInterface::class); + + $this->localDirectoryMock = $this->getMockForAbstractClass(Filesystem\Directory\WriteInterface::class); + $this->localDirectoryMock->method('getDriver') + ->willReturn($this->localDriverMock); + $this->remoteDirectoryMock = $this->getMockForAbstractClass(Filesystem\Directory\WriteInterface::class); + $this->remoteDirectoryMock->method('getDriver') + ->willReturn($this->remoteDriverMock); + /** @var Filesystem|MockObject $filesystem */ - $filesystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $filesystem->expects(self::any())->method('getDirectoryRead')->with(DirectoryList::ROOT) - ->willReturn($this->readInterface); - $this->object = new Mime($filesystem); + $this->filesystemMock = $this->createMock(Filesystem::class); + + $this->object = new Mime($this->filesystemMock); } public function testGetMimeTypeNonexistentFileException(): void { - $this->expectException('InvalidArgumentException'); + $this->expectException(FileSystemException::class); $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); + + $this->filesystemMock->method('getDirectoryWrite')->willReturn( + $this->localDirectoryMock + ); + $this->localDriverMock->expects(self::exactly(2)) + ->method('isExists') + ->with('nonexistent.file') + ->willReturn(true); + $file = 'nonexistent.file'; $this->object->getMimeType($file); } @@ -62,6 +96,14 @@ public function testGetMimeTypeNonexistentFileException(): void */ public function testGetMimeType($file, $expectedType): void { + $this->filesystemMock->method('getDirectoryWrite')->willReturn( + $this->localDirectoryMock + ); + $this->localDriverMock->expects(self::exactly(2)) + ->method('isExists') + ->with($file) + ->willReturn(true); + $actualType = $this->object->getMimeType($file); self::assertSame($expectedType, $actualType); } @@ -79,4 +121,29 @@ public function getMimeTypeDataProvider(): array 'tmp file mime type' => [__DIR__ . '/_files/magento', 'image/jpeg'], ]; } + + /** + * @param string $file + * @param string $expectedType + * + * @dataProvider getMimeTypeDataProvider + */ + public function testGetMimeTypeRemote($file, $expectedType): void + { + $this->filesystemMock->method('getDirectoryWrite')->willReturnOnConsecutiveCalls( + $this->localDirectoryMock, + $this->remoteDirectoryMock + ); + $this->localDriverMock->method('isExists') + ->willReturn(false); + $this->remoteDriverMock->expects(self::once()) + ->method('isExists') + ->with($file) + ->willReturn(true); + $this->remoteDriverMock->method('getMetadata') + ->willReturn(['mimetype' => $expectedType]); + + $actualType = $this->object->getMimeType($file); + self::assertSame($expectedType, $actualType); + } } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 1fdde276e4e51..07a0d1345a301 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -9,7 +9,9 @@ namespace Magento\Framework\Filesystem\Driver; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Driver\File\Mime; use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\ExtendedDriverInterface; use Magento\Framework\Filesystem\Glob; use Magento\Framework\Phrase; @@ -20,7 +22,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class File implements DriverInterface +class File implements ExtendedDriverInterface { /** * @var string @@ -81,6 +83,26 @@ public function stat($path) return $result; } + /** + * @inheritDoc + */ + public function getMetadata(string $path): array + { + $fileInfo = new \SplFileInfo($path); + $mime = new Mime(); + + return [ + 'path' => $fileInfo->getPath(), + 'basename' => $fileInfo->getBasename('.' . $fileInfo->getExtension()), + 'extension' => $fileInfo->getExtension(), + 'filename' => $fileInfo->getFilename(), + 'dirname' => dirname($fileInfo->getFilename()), + 'timestamp' => $fileInfo->getMTime(), + 'size' => $fileInfo->getSize(), + 'mimetype' => $mime->getMimeType($path) + ]; + } + /** * Check permissions for reading file or directory * @@ -300,12 +322,13 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) { $result = false; $targetDriver = $targetDriver ?: $this; - if (get_class($targetDriver) == get_class($this)) { + if (get_class($targetDriver) === get_class($this)) { $result = @rename($this->getScheme() . $oldPath, $newPath); + $this->changePermissions($newPath, 0777 & ~umask()); } else { $content = $this->fileGetContents($oldPath); if (false !== $targetDriver->filePutContents($newPath, $content)) { - $result = $this->deleteFile($oldPath); + $result = $this->isFile($oldPath) ? $this->deleteFile($oldPath) : true; } } if (!$result) { diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php b/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php new file mode 100644 index 0000000000000..4634e3dd1018d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Driver\File; + +use Magento\Framework\Exception\FileSystemException; + +/** + * Mime type resolver. + */ +class Mime +{ + /** + * Mime types + * + * @var array + */ + private $mimeTypes = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + ]; + + /** + * List of mime types that can be defined by file extension. + * + * @var array + */ + private $defineByExtensionList = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'svg' => 'image/svg+xml', + ]; + + /** + * List of generic MIME types + * + * The file mime type should be detected by the file's extension if the native mime type is one of the listed below. + * + * @var array + */ + private $genericMimeTypes = [ + 'application/x-empty', + 'inode/x-empty', + ]; + + /** + * Get mime type of a file + * + * @param string $path Absolute file path + * @return string + * @throws FileSystemException + */ + public function getMimeType(string $path): string + { + if (!file_exists($path)) { + throw new FileSystemException(__("File '$path' doesn't exist")); + } + + $result = null; + $extension = $this->getFileExtension($path); + + if (function_exists('mime_content_type')) { + $result = $this->getNativeMimeType($path); + } else { + $imageInfo = getimagesize($path); + $result = $imageInfo['mime']; + } + + if (null === $result && isset($this->mimeTypes[$extension])) { + $result = $this->mimeTypes[$extension]; + } elseif (null === $result) { + $result = 'application/octet-stream'; + } + + return $result; + } + + /** + * Get mime type by the native mime_content_type function. + * + * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' + * + * @param string $file + * @return string + */ + private function getNativeMimeType(string $file): string + { + $extension = $this->getFileExtension($file); + $result = mime_content_type($file); + if (isset($this->mimeTypes[$extension], $this->defineByExtensionList[$extension]) + && ( + strpos($result, 'text/') === 0 + || strpos($result, 'image/svg') === 0 + || in_array($result, $this->genericMimeTypes, true) + ) + ) { + $result = $this->mimeTypes[$extension]; + } + + return $result; + } + + /** + * Get file extension by file name. + * + * @param string $path + * @return string + */ + private function getFileExtension(string $path): string + { + return strtolower(pathinfo($path, PATHINFO_EXTENSION)); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php new file mode 100644 index 0000000000000..a93d242dbe15a --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem; + +/** + * Provides extension for Driver interface. + * + * @see DriverInterface + * + * @deprecated Method will be moved to DriverInterface + * @see DriverInterface + */ +interface ExtendedDriverInterface extends DriverInterface +{ + /** + * Retrieve file metadata. + * + * Implementation must return associative array with next keys: + * + * ```php + * [ + * 'path', + * 'dirname', + * 'basename', + * 'extension', + * 'filename', + * 'timestamp', + * 'size', + * 'mimetype', + * ]; + * + * @param string $path Absolute path to file + * @return array + * + * @deprecated Method will be moved to DriverInterface + */ + public function getMetadata(string $path): array; +} diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php new file mode 100644 index 0000000000000..4e34d497d86af --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Test\Unit\Driver\File; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Driver\File\Mime; +use PHPUnit\Framework\TestCase; + +/** + * @see Mime + */ +class MimeTest extends TestCase +{ + /** + * @var Mime + */ + private $mime; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->mime = new Mime(); + } + + public function testGetMimeTypeNonexistentFileException(): void + { + $this->expectException(FileSystemException::class); + $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); + + $file = 'nonexistent.file'; + $this->mime->getMimeType($file); + } + + /** + * @param string $file + * @param string $expectedType + * @throws FileSystemException + * + * @dataProvider getMimeTypeDataProvider + */ + public function testGetMimeType(string $file, string $expectedType): void + { + $actualType = $this->mime->getMimeType($file); + self::assertSame($expectedType, $actualType); + } + + /** + * @return array + */ + public function getMimeTypeDataProvider(): array + { + return [ + 'javascript' => [__DIR__ . '/_files/javascript.js', 'application/javascript'], + 'weird extension' => [__DIR__ . '/_files/file.weird', 'application/octet-stream'], + 'weird uppercase extension' => [__DIR__ . '/_files/UPPERCASE.WEIRD', 'application/octet-stream'], + 'generic mime type' => [__DIR__ . '/_files/blank.html', 'text/html'], + 'tmp file mime type' => [__DIR__ . '/_files/magento', 'image/jpeg'], + ]; + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD new file mode 100644 index 0000000000000..b361f47e9c25d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD @@ -0,0 +1 @@ +� diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html new file mode 100644 index 0000000000000..2b699a9062611 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html @@ -0,0 +1,6 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird new file mode 100644 index 0000000000000..b361f47e9c25d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird @@ -0,0 +1 @@ +� diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js new file mode 100644 index 0000000000000..d168db0daa734 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js @@ -0,0 +1,5 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +var test = 10; diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento new file mode 100644 index 0000000000000000000000000000000000000000..c377daf8fb0b390d89aa4f6f111715fc9118ca1b GIT binary patch literal 55303 zcma%j2|SeT*Z(zRXc8HsXq^y~>_QR7zGrA`MY1PbSt|PwLS!domn=m{icm^+k}XNf z8j&UY*8jRy&+`6%zxVUb=Xo^DJ@?FgopZkDd%ovf_s{n~zW`Rn>o!&ZsH*Y+EcpBJ za}qErxSHF#0TjT%Kc52N=NBrzi!LsgB?JT<ocT>H9L+5G%^mFpuA5#K5aJgU0H<WG zUp6(jwRFLoSz6mTNU={>RI}r4ETq`=MKuLAFUwnAv{7+)vOME{nr!ZFYkty#T}B#z z>bk^r`^)y0E~fbF_I3`=64#~Jw@xkrpCf-3V8?GA;$kbsE{l9B-awOtmv?ls#EbF^ z@|g>Y3E{<#^9vmpJbC;iFJ4$sNKimXSU^ymPe@QgR9r$(2><)R4nNJw!b(C%LFxBr z!QZ6Ve_zzKYuEU%iSRo*SqliAJb6+;P*^}%m=7Mo=j`U-VtSp=!I|UF85As?&7Ev6 zyVy87;E^+$nmM|<NU_6j`s)(xFaLGg|F~BFI8ambe?QdT9$A2`(>lB8SpNNe|Ko|B z$!?b|1#~Q(9bKKwE#cuD$eS-q$U9k@x;Q$K9UbldtRm^6ql=^SMaRo{`7@&YLU>J6 za~lWbFI(@>)Ra(laCR|uFt=1ykYb1D;J2}{kWf-MA$meiQB+n${)CW_qLA!yIVE{P zF?nHmLHXlKLV|zZtKewvYH#V_^5?x4f4^7kzrGi72K&qKlNBtTY_3{bC^<RW<F_ta z!sfq!7m@#ZzTfY)`0wB4_<wz`0Q@onh-v>}>OX&i{)0UI%W>f^f4RP;1N3(%=-5BM zgPmLM3M^j007Cuj1O9*pgTYW^XsD@aw$s4>cVKB~usi5zX?M`l(&6Z~9yq$43^<0J zbh~yjGw<59f8V}+`w#u~KyBN$jh>F46^CQp%fiUA_pcZJuTOry0E}3)I>wI*#R$-h zC@MzO&)1*=pilr!1%LeYK%p^IG~1}LDELSC!oOajqM^oY!vQoJ9)!kVX}4jvW2jI7 zg{HzVQXdv#qLJOEZ7RI$if<9SQ!%qxWco`v9kVD7XBWR3ku%3xMB6iNaDI6wZ*HNx zn`=*W$r0`u1+pR|{03+giiR4Ep+P<nIUgex2A)s$3X_PoFT3zA)1sNkbdGj89Vcd% zmtV||bI$tx`~c|SN5dl-fee_m9(8=@i<c#00G<E{0L8{ny}!#sjUl19?3HnD{M*cv z()6jC;?f=y4mI@+m!*TAoaK6IrN<iqC};|ambDNFCKrVWC7H(P^GW`A{5d>jEonSA z({e6a+SY_`)<{sa_yv*YD9DH)g@ha>2Bm>}L|O&f2SFhqjgh3JLcyUaDzw@}@Rk<f z8C|wk)0hU(l96XvDq^-j4tbUc0P|ZOfJR{f5rELV*T4c?FndeJ3mzthP*`z5kC%$Q z$dbZ@rqWas*5;M7jPfn!NpK=0)EYDwYYPXN#~E`e5h(y=KruCVtih|ko%qHyM*TKP zFtIrJawE+yx(p-xix*mgLo(55kO`i}*qiSwecb=Z!Il-L3P{)n{I+Ik6)$20J`D}X zfR+L<zHESs&~X37Gu6NrHm6&o4%xkd`L@AcdxeMiL_M|glAKl^7SiXmh}fx<U$GYf z79tjNpS6H9;U%uCLRjc?*q*+?58p1Hi#@rV%J)R0D&*banm*4KH!GYDA;ec(6;N{g z{4FdnEEteR8xlkbAk(O_02C*-0Y{<$9<;9-0BL9oY&l$@9clD5)I0#-XowbD|E0mz zlQSj3myV+WtzZtozCf0e?$~Z^v@ekZ|2mHmmxscp;ZVA47`!j37)>1_?C<o3t3FVK zUw4;<JY8`++ZRAkK+YjV3*Kg2R17v747FlYczS==_CRg^?U_ceEYyQDjfw+#Lnsu& z3x3Oe25+Y9#qIVbzQE&Y0cX(GlF0&ST53Q|1ZXll1^69Rx?EH{!+V;^^qG=vIE`cW zXws#ouN~2BN9uwX^exm!O9jU?Ma<8mgFYC&uK!ZTgEMN;>kV|xKD)<C?QB+C)LQYX zZ6UplW|dV!+<Sio5JF+p-OWVC<8|v*vRGJ{(-fe6$RLY&jFV9fpw-esBLN=JMW92F zZ^8&2i25z!V0ZuosnHi`zy-KUBs5@817I&2&_zPqUQm(+0DYV#m?#W{=?rzTHweCR zJSbL>gTV)oFJR~ft+(@E<ZwMZ(imCHS`b91md={P8URqNWPpm1Q+u`jP7}fQeVCZt zO-z_mFCAM*8c3sV&a`Fif0#XW!`T1>m;naAa`Vk91wd;O05l$F9-4yANaG~Sy7HVn zR<7;I7V8)&VpR6EbtZDY!+ESKyXV8V@t!k2!<W{bI;&4sDYtpGbai$l7e^0z{&0)? zTENOGOe-u4Li~MM)M|++5_3>a1_cX)P7yAXE?Gf`!Yo9lWDx-iN)7o)9DEI+(~gn= zkrhpW%YZ!47XY0v5nG=F0Am`8nDYXlab^I}u-qc}UZ6$o{o8j4+PCn+$95Y4k#La{ zs?NlIy=P9D<}BFSjAERs4##eIiKeFTged<N)}|A-?=O{DoZ!4<7sy*n0%X*nc4??X zcgNM2&mN!!F{F%~vKK|$h1({7%<pOE^}4(~R<kCDM*Abk1Q&-rjR20CqG>4@7zV|| zmeWVZ1@g{pFUT@F%t*Y)ECezX@oemO{6@94sd<C}wJe}!X3tPTX+uk*j{JfeU*bKm z9Yv(!iGY`Rj~P#d0R*3c<EX8FAhwXu#}$bHf}1Z1F#57VCWmdz!O&4SV2A{T<}u3= zFhEyH7Ms(^PG~*VoF4k@vqS!NCk}YgQ9LzxwMfPQB4E|SLl#(L6l!@_bW+r?ct4Y< znEWmlCBTbkU<!L+e)UPx6Z)15jJ`UK5idsb_v(0t#b!0_FrlBg!t9px?ct9;^VELj zxAya|O!ACR%#2JO1++LofgS`fWEe@jcv(3>rY4f8(d@qZ!WsS~j4ls~8O<b2Br~dE z6ah64h8eGG0nH6+qlh|q2v^V#(3I`$(B3E)LOL2C);>-{4vs)6a8Uz6potEE3xT5r zN~+L<eF2RMVs;{o8w4?4Fk3JNm~pb=G8l;*fCc#D@)EBQTDeG3W4`4Noko=2;1xXm zYM6D{A<}@YIAB$e3@r()49pjk_fyKQH#5t5;d=o!xJ^{dCCSFBLbsGH+u@d|hOn@# zZ&C2sn5RjiZ_X~BKD=-&Pe@(<TgaC=jggc<71X<M+VdYGNGO1%MWKiQTMy77ID9(b zq2dH6RYpc)PEI;AibdO$Sq)7sr-;wVxd#`8$bylT6A0GTW>kZ=S5#nzOF*Lt&jExW zoaMmihgS+NQnOPtKBm;@aT@qvkqoUo8ng8ELeq(lfqVE>T141<Q?f`{`NI$?C?JDc z4uU6kH$Z?i)DHk87>MbZ3uZ@@7-`dBBEpmCR5)l;j_i*V2^1dnEq`#NF)WmLV+WmD zo)Uot27xq0Qbu3@znx+fMgkX4C>9W2Txt{vc*v%Br{x^Wr-UbEwu#Tk(MI%<yj=U9 zO1D?*dnomE8ihZc%fao8yRNfXgG5CRz)*wjXkmb2)+WG&z=Dbbr0qOGz#@dANW|tD zsA?0iEV$okmqe|o5P{=i5|%|%RLP7{cnn+xZ6b<@iNwGafw7Kf2H<T_nkb>>%-g5R zbl-)8G)f=TFC^6JSiG!l{aCCgO)p&4)1n-spRE;TvVGoWXux8yDB)6H`Qy+JR~V|F zUETQ;+`PFzWYsAvuI!9nrS_`CnsRzY=F~msL;7ErOI`V{KH)Cv&Wz~1YgwcHx#M$( za<r)XNxL5J>|MG|R=X!m1)KO4Ih%DOa1>ntzaTYZ09<rDXkdk?9RZ_qEAOM&XpWGA z0!UazR+zhjQIVua2>O`wU)+~hW0dp$H5L_Yh*8A8CID<r8sMPX{ulHdY3}T$)6j)< zfrJI<XpLw4A3e*I=nd^!5c@{X3NO(tUJ=(_G+XBQ?x}`(`)jHHmAgU+F*<-qM@gea z9nl7K@$6}go`8|u5E22hnAu}USj6*K7(t$&Fagl%q9SoH?UA>ZIaU?`GF~T56$aj3 zMrJyrDE@8CQ!0&zg?ozUpKA=Vj^tW?*U|QKib;tK8SL}+w|~gx!P=6XU-rEGqsUz+ zaeZD5>snHEY1G2E$-*}tMyDffuTD;e-@Mf{yui~_oLsVTFzm_HIq$yEqM4j97XC}e zZh0q2+plxW=WB|-ImUYPR*8M#$;Ts4+DjkKNZu+D3mcK-W+)jQ|KXIcs=6WCsQjw& zvu%H$RNQh^m5lIeh2*yxx9pX^bFFpeYnt9aSgI3Z&lfO!+1N94x96B|iI%68w7|9S z=Ir;A)62*68nj+7a}^fy<$c$B-6Pm|q;20W!J3i!lq_9*010LQA{OE*5p@9u0nQHv zfT=-o&~A{4uZ@McN+7ZSA|e)KUYJ?wEL*5t4b)@zvF2kbW-GA~3Zzgu3J8T+pyuhY zId6KQHHU5z&*ylNGZsQZ<O|S^@qh!+Z1!_yd!6Mj&2+gcJ#uws(4D2^bE#2#qSX~% zF1AY{x8tsB*&*}|CLaJGEC6bw5K5#rz|%k|1yMN^8kGk`9`+n6E*|16I)W4$0#h?{ z4obyWH;Vlzk(M<N9SH#Gs3M@GgAhnbbFw1f$taf&v#Pc1z=e4Th=gP{>3xiOt{JH( z!Z#KE<l4hUn<4%0MLv=|uRCQjSG7J%?G!+-Rb(>GdfvTlDSB;b{a|e0M#5veTtg$N zO{-LR+}-MoXA0vdp3}Q~RM^j6H@a=PxU{JJa;fdv8~~`W*d9x#g8JMVbyX#Ga>tBK zlo^*0=V{60Lr*;)*Hsj9TSdL8NUDwA=uDlcZeC&77yIs*L%&z_%#8bUhkK%v)pnl7 zJGerps*em=pWzV*A8oNId?>ByxlOV{G55q{v-91RrO&NO?l|d276$)lIcIiGv_IFs zW~7B}!aTsTxmGR*K|^6^iWd|J!59lhUjS%pV~CHa*=TrqFsx*PFb@h)D?y&5cd?bV zDUq&Ek7-!rL#+Xk&P4bDmMKe#_jhPAJ*si^^@m9!w9Cr%vb5EcH^1Nz1Nj&D)VJ~2 zOTy#BwQJJl!(MH{cda^j)igww-ki^tjz4oszW`zvOo%YusHKH~9iPdVkRTK@Ej5ho zEOh7%9IYamj3!{TH56sp*iei_lo|!Hp}hPA$O*};c`$_l3u?k|0Njd96wn4yiHbqW z^zlaqEpZUqBJT&^)eUW;GxD5zb=dquhvfJ8(U(pOWeo!UtH-}SX{a?9?kcgjVS7Ay zV5#i+lQ-#g+-^|~6*mluHX8*NQpY@RD!v|PDeUs@UA}(mys}Qh<b4ZI<H7PTMjW(* z>DY%H*oRE#v%;;|Ii8I)k3>QshL4NiE5!QBhss69!``lUFRKZ3usJIwe>hq!bg{|m zVRz5T2Vcexyd3#HR(5jO{^95#KY!`Q<2^4|1{hoCRyA|`wDM%~`-WV3`ilFbHx5`m zNIn>T!)UMP-es-5mloL1xt>oAE--cK4ai--qaoZBsVc%k;i2G}iH9Lu18f{{?d71b z#af(4jSi(V#4u&zf^^wfKqw%aDL|X5#xdTtuzOH+$LZ)kGlMUQk#PnE_ZVjhko&Mc zf_O_yQF`6@<wj&^6X7OvwrzqX?#taUXTFF*K@0!CLv?XrDs#N!Q%A+Kg&qWM!Oll+ z3?wuKi-N2b;cLu*#fb+EX)A>dMP$Z?P$0$oMRO>(*fVHDLPNHA4whDd%tBHmU}9z2 z*a7hm&*6a?8fJMcHS06ZZTla-Z9E*>biV1GNC-?==OXjlFZF$n8tk)b>^Uktxluhb zI`(va*x|H+LAX+W$5Mw0)uMh4MA&HC#ewngiMbUiwbIE#yPrTgbLyOQf9cQz&%GR) zK6iUcxyNIkm85rMBo48J%D?V=(sTENQ-3?Gf1n+^5M$Eg4qSU$!EN(N;-U5$zhj`L zZPLc|x%fuU{ROFo*Bo7|1WLkNstQ_H*ALo1UmRU1dcWDDuP0qD&r>B@eRJL<7h>&= z68-)Ise3BwMmnso`MkWZr)K?9)XUJm$NgfBX&i!YuzEGTpUa{Ot1cmS2$X<qN(APg zWg+Vj&c~&Z{+JP)gSmjtgtRUvA)-O0h|r+Ycto&8qm{RAr=p03I$MB76CsThRFg(S zBN1RWY#yh*^Q6WbyC(Rv#;);j?a@0LB|%rpa&K{3knsp{!5~!ee?}@1)y;5t0zl68 zcU4oMNL0xQg2e<aNK=3aN=JsA4Mr-)kPXj+g_Mngj-_r!j2VSx{#~;DNu6YvMG2UF z3NIj0gJF5u(CIwa6*{NRGVat}C-T|iMw|L7T34*s?|GeA9Y2+SLa&P9op+Yk`g!`L zYs%KOb8{zMx(<}|&A973ES_#JIdUodgR}mrBqn>A&hXLo`|7phs|#0``UYp*_V-Oq zelL9aKD-h_EudmMEp!W}V|Bd&Or)@czLQ&Yz|F4k$2p^x{5z-DSY>Yh*yLy!alO=0 zoJxkyaldZ7yUc!#W3~Kt&p?0M&7@PIx2pPd3(sAd`ogj<B^BM&)g{*5o3P*=UhSyW zb7_!+L2YXD`v>uyBjMH+jp~Mjy*0Hn-cidtZ-lk<luJr(GVF`2ttwcaT(0U}sJZ~Z zlBvCDzft}Z9@iF|6RMXI=Z`mt28XJ>Zoc{|V`)!Ztif1xvFaIX@nG?_CqIFXNz&Xw zZh<Vl{_-{M&kNW4Pn#ES6wSIwPBg!)j}x;zld<eUN^D<0_U^WaLz2#wi?1smda%zX zwwgBSDOGU==Ub>-?Otu>VpDjQdA!+*O}O*5P7q{@*ca6P(7((O_Y!=dmPW$X3(*Q; z14y^9(7n=de$;aHbb<E-8&sMI4H}KScdW9lcbf~gsI(rr&32THCVdwvXIP_Pzf%H& z5&N@klZ1n=s44B@9b`Sp+FX307lX^GPXj`L8I5EaNBhkW(#cX&k`<}OPSRtq)4;cF z5ev|_C=eq6VA6$<q0bJhW(64XkeXo>(J`c2A|p8mqk|?i;K*bhUj+XBMyc6(Xd5t) zyOEUCp(L>7U$D4@#V9aV3$;8RxQ*_+2$YISpV3^=6XEA|FA*u`2)jw1uJTN$&R;f3 z-?v~>I>rK_q$M`X${IJFTsd-2^r^+Plm6MK4^(9O3$uR$$zh3u+$tf9lT)cTYKIq2 zrZiqvuKkpkotGcxDl7LCVt!T>#C!*Kb|D5HE9)nQvCyulS&thPSCgaYEexlH&vvtx z&L%DUD0*@?G>m@hKz!HhlsDgEoAtHY(WE8UY02S_ys5o2>q54U%JTy!J#UIMO-PC@ zUeo`)bbX!2cEf(wvwT+XZn(7FnqbS|%&_@{(TQX+eNjuN%f$i)tpZA^P@;3Z{oY)( z*|cxXr#ZCwe5n1P0DJ69OXW3{!fun~xs<V8`OR1DrowME4!hK+e7LIh^6Q;_fd{=$ zIbGoo>uib{Tv(UBk=9dO7TWF1uy=JzwKkLKy$EztD&lc*uV}meD;sZI8K1}`NuLQX z5FQPad^+F9`q;8e@0;!2?(_B^rA@vkndm8P({o@Nd}mVWw@~Y0LVL2+M72`VW<h_o zvQ=FB;Tqq3+q`Reu@CL%_Mw*ZG3pM_!!(`swE2S0CiuT1IOTG%=xvp&=o-N^aF_;` z-K=^?Ks{O*>M@5AeJke$HSsXU^vvrtS-P0cKzc)`(3s{74Usb<w}cSCWO`#7uN|K& zc-tb0A3CIeZ;X7%89|t|Ue?z^e-twp&XGl@Ur-{_qJS?ER>N4D@1Hi^xBM~H6uM(A z&wof=dtT&ZVQ^4GU`9d7Q@8ndiOR_og2zQh{j)~>&kjk%?GxVbT*UrbsOfCehE;w3 z{!+e%nI01zF-0BAUP+&(jSlB4)iU`z=x0@b0$b6ETZ{S+Q|flzJHJu3YP)gXnmeaE zId=8_sucId_lqs}4`|;|xD-C7b$56}I9|-f{rY?t>jTp-Q~mbQN$Q>-JD~^C+fCK& zpxSGAGF~afyw%y=Q~h0whgf%H(&@v=M{Qe=vBjrc+m^uQcG{rt_<1RY8;yw@!&M>c z12+>sJFPx%>nJRi7O)QMFq3rbli8*nCQ|HF+;Fm&`@wy#KF4=`r;gbr+->R#`BJJU zp?X<VGvSrh@yz`NY!Z>Tm6^xS-ylSp<~vAO)tBtw=j}c6c=$o+j(g;uQkFNu=Yy4_ zrIYt3eY@LzxuZ<+suCZA{8XFJ)%7tt`vzX#x|f<ifu;CF^rA^_GWV{$4r|NJBp>CQ z_2b?5?LEv_EBvBtb!)GSbh|aeiv+G!LzBk$uT7L6n@p(M(;XM(Q+QBo<kY7A>7KH( z@_|FMUeCQ<7Z$H|jaIb2d~SDjezfPIu)BWrPSK{J8SiXoqZIo2wk{+YXU1w)JNmrL z*OT++ekHh?aZb1Y8`P4UQbaUhQZ~#sOQXT;fOQ52{~7=eo+7{olw^a+?1#x{MkNd_ zMGnhyNZ1!6jH`_x11^JTx|rS=HPd)Q#23vhFTbp>u{2{9gXz|+mj)^wq(>Yuu~Ud? zj1&loP93)XrAjVOSU)`T;t`2f74wSuu<|hX**=aVFOvqH+69|)eq*E`R20(?4HTy0 zVFN`x9#Z6A7#YjVPGQgEA;e&yZZz|^PyT0+B(hT+DZZ~P<{>@`ca~4lN77jttx3Op z?s#?ni|)On<5n8W`iH`6S0k6MT-WcH(YWgAc}RdGR#Ut0(|k(*q;clOE9JIRZu!ZJ z{ER-i2h~R;90X4H<d;<q?72}tW@2saY8<cN&JYBRMDtFwEW_yi0hy=kwS6=Di=GUZ zICH#gkm;9Q9j+|&J*Xv+*I{BQo$sTZ`DVPk+P;Eg_4)MWkK$5&+*52zjpN>SCRpVz z)&lI9_?+VtXPc&vT^Z#=EPfBX_Hc|OiJO)gOR&Q9GgBzPJx=4@TMEnmGuWYIC2c}a zahOI%Ltygz58v{{6r)TRt{$BI^h7LAYs%p0Y+X^MS@l**P2H7iqHpcx&2TgNWp?YQ zZ&CuL-;?jV7aFO%RPx!qNo^^X*%Pvmd2h&B^m}-=hd09xpWUnXCvLahnmlx747tEP z=IvfOd0&+pvmf8(ioSLJal@t8Z;sfC?7HK^KREY}$*JM}aDM6@-jmgdoBpW+zA1+U zGG8x#+tfN2);TIE<|D53<oSbB!R8}fJOVe~>9$<_3C5MB%5=91xGsU+p|3~xv7F2O z(Jc1r5>pqeI+qoXAl?8v!WKjxf=L?{fw5qXm6QAJj;LS^ZFmBaVv%E((}*Y1+&e?! zxDaHfBum2!YXLGZ7Up9-jsVG?ARh1}-TG`0O`j-oWKV5UZW+B^ci&GScs`Zt+Gt>5 z%?kJH)faz~Gb6&|5mgrQ`d`$KhmamZVb3LDGf>;v{>sfz!e^nj@I#7l4o)H#MPgwl zAz~dF6aCp>@3rP$15^Im_x*)sjtAES#Pb?wN7MJa)(E@XK1h`bS}ND4NpiNmI`ndQ zVW;uJVCk&8C9|%gcHSMO^3RV$@lmSoQ4*KBVZKFMSvm7IM?edtd4{c(i-pBKbeifO zEe$+=qF3P7OZVQnIXCzDx-J>+_RKJ^lKEFH;@{^!T`S`$uWImHG4K9VY*Hw2=tg9V z(`xMEqF3p%)F*M8#5Iq}73Lw%c&g?k=}GZpney9<zqGM(nhH16Cn~TVWxrrdq~3?E zZK0z(ADS8B%4NxP$M&3@mA1tz{r*19$R`Pn6&Dr)BJEXW&L~aT-ML$5@u<|yoawu9 z^{AfW&J%r|#-$ILn*3J2jo1CCV)poWx=5-|CaGa~9;Rg<?sU^JGpnIGj@|G3H8v7n zPK0}$Z~P!)*nG<No$ZM`ub(x)r@)*%_I+)4Z-w@EXPYw}?~5+q^$vZOC_bC}MChVJ zN7d?>XnfSPhl^P97zcW{<Y9GVk*M}@&eQ|ayTV3ihx8gAoW4B1Tyfv(>Eohoqi{bH zve+Ju?G~ZH0Cqcm0o<sr84w1cH<)r%6nOk$sgGr*V#3yBj#|*L@76=}2nXRzNj$<> z13WDhm2q1x1yvgyFHizhP>mK$+oL~D@c)a|Z8?q;4}hdD4NBf5b^>!oAP)#hM`uuo zmN_gKgwv(b`k^ELv)|C{cH*U{3v|dgs+f)yFs_hUo?X3prd_(p?fK4!OLfuDQ~Da< zvfLCA-?QVOA)DTt#KTjD;u(UKgGDgo#oCD7RknM+^z>9y)flf=6#r8vw>W{joBk#O z0Vxau53bv8I+}-f2=)04k0<Z$w<{>udD2^HZPDlC*xmbiz~Y0mNP_!9mV%jNlCJq` zm<aFSKEa2a=E4sawAr7I*j!F9pRTm55f00Oy2|zmbHN9F?YPUW?raJi!5^bXE&W+O z72V)hd7r?whv(hJkip_)jf^w&=dNrhU9mc0`NSb9yCrgZaV^>L$2U*g#CbLDp1Khe z@@R2;MYwU~E%|m2=VqyjE0^n9J9dc97;7z^@yX|I`fRQIdLpK`*D-sb{mY!Y*LdIg z4?lsvgO+aY#Pb=5O4p}7)qjF(RzHFK1V{Kp)kf9n)cb3K?ddbh(bfa>Pv=C{g(y3y zVCU1H6?Vuh_3%`9EKF&f>@0G;c$!~1EjS_z$BQ9b;_x-Em<TTd{1A!H7b-*nuv<!? z!J{dE><vHhj})g!Bmv#}ob6CKC9rH&`_zQ=AZqp;BBW7<P}u&fK=kt`>skc)5;^l= z6BPDgsnIbwW&j24fakZi3nWK5=5N0?eYCp6`6swtl_8N5ym+qVv#Vp(spPMVDJo!R zZok)BKv5sBM82D3xLSPq5UZkkbWAn-sqfaLg4ApdF|Uv3S)ME|>5f*!O_xs_9{dTU zm#k81)dMcC3Jp7-O3__xwHNTvItUAX|N93H?eDERTt=${3FA#3RSp)__mO6dh4!o0 z6Ch8^n~oVx$BZ16;$Y40b9@_FT-wAI?=&?aP$KN^ByKhP)l=`3$@G!=YUL5BDsH8u zUutgS@WP2gOG)d&u4zv@qXVDr+MVqp*948Tt_!4Qt&~?Kc33EyGmbls_wl)Fd~iPL z>3*-^W~AV}Y4ZRT%Ozv0#Dv#E*Uo>OYAJD>x^z&%NZ?G7Jo|pGD(A6rlkFXspGquU z8R|MvSy{*s+f?<i<M#K~I)i~V{p-EwitE?brLJ3aC#iauj~$Bmane}QDy4U-uy$#7 z<#Au{fw*O9MMH`4*FUlgOZf7pI}z<+HqnYF%Fmp`uW=VbsBKI_A6s3qcr|Hv;Ky^j z+s8kAvO<3zFIxXv*crXT_U(#9?82f}m;TF!&u6Udt{P74+jiN$Z}iZI8B1=~s#}Le zI|%BUF4pRG!}|7Ky4NKipVKX}?ht=W0kc)hYhRK#7;<jQ<asQvNO-4CzEVH*V$N>u z%d<lkUA!+)+OYQ;yw$FIAor`kEew+zJ7i(Bp;%dT5g`j#{K#KB2I<JCWTJOt(f+KD zV7)-fK%{yiHB>cWYU7}VZuv*@$He~eOXzAA5QMh#$f^;{P%3$_!-8iI2_h&d5fw-T zbQ<g<>8S+W0|Ya~D@g)D%xTm_M8)76Hc<7PjP^26uUgUEOiEpMUD$a)!z=E`^#kXo z!?``A?neQtI?vj`%Av@0o~qnNS4VHp^!E7yt5YL$MH9o<7vnSOSHGRzn{6m;&%SdX zpGxatuCwtL_Xe$5%;Og8`u4T1nO&b*G6-H@&le?ahL6io=C3EqSbhKAxNgGDk)Ub3 z@e^#zzB+x;UXqubceAR=c5KA(jY<1V$nwtkTQ7UEB_*NAeCoFG;?k1mAy<wm&qBT* zTxYKxu>A=b7#_5qd+1R#{8hrF?1!hs!SaI#QnJLjHf{WmDYF~3l&ZR4i`P2elerwX zarLSc_xtdqgo*R>6};(eif>d;RMATx&3t#qlRy5Ww~?vkv*9UCGfSy+_qGW821C&7 ziWgVe)cp>b_VL;B*!*7a_il?;Twk78Jhxl5RLqap=c*Kn=c=N*J5cxJ8LTcB7wjJn ziQX;AKP<CrEb+<fC$>e$ZSPo(w2Lsla`jwhQ^<NXI^6X#DWBK!tGAA_W~QjmpiIu~ z`x$G_qcakP0|!sfdob{VZDH5y2I!{0dMRAlI5SYz&@ulLSnV~~+=ifB*JR?oJ3v?o ziX2FnkA^o7w!8&}EO2U}(48#EXhD>=FBVVcr7#8?$PpTYV1pZ*iDssR2nt;w0(5!= z%u;a7hXT96IKYVXRG}xpCc(cu4(wZ{A!i;Q6*a7fXJI6xqFM0#7j$SDBZ;((L>6!q z$5X7J2tCt~9kzZU%#o00{hLpU{_(91;$8j~(=h|%6*p&x78pC{wH!V<QssNi4o_P@ zo+AP`hC4nU)0-?N<l)uoN@>#9Rmajp;>~Z2!uPhR?F;@IW_NdwVFp6vd+r|+mpo!_ z+HvfAJVDW0Y@Ko5rUNPV;vOa)Q)#)6n>MZ-DUjNEQnJZV-1+u#lkb`H)#}3%r@2)g zdR)EQ_Uh%JlY2~GadPDBwIF@0oS;A1lqJiYB}4sf<m7A95si^ZA_LD4foaj--emH( zHyL@W%-oxhxAJcAKQ?A54Z8%w&=7o^6YcBj-W8GuqbHSHT3ipg8>h{uVrjBRN<TXo zwCxiM8aOi(e|$s8vtyb5oJ)^|S^FL9Li=1q6gPf3((Sz?PN|DKHsopfp~a%}g&ixW zJ>uW-o$vQ5nwj<Rbhvl!s!6d==7TPa=qpiIqTJO4ZfJF1zglqcf%?^F6E=6O=^hN@ zJ=bIG^VzQb1U!3wg5Z`-^VOh<a)B~w2cNtRxW?Ox=U#y8J)T}Ot+3&_z*x7S<vqZy z)v<BM#(eppW~!&SIpRcHt>Stpr-PazOB^psN%j`4FU?_6nj-9=Z;cSV2#COOFhNj? z#bIk|U{%ONQIdsTgCl5T(_mW#!K6@FJkkOJ4h}&OX~FNQqpcYc$dV(VyztA1$Y>s* zc;<K8Dny%@gNKT#Zha8s)JLIGmqQnX3=Cz2z!o|UuN^OE)m1HyKcMu~=b()C0WF2a zDHtVXFiI9Zv^c@Gh?Rh)s-NJC?!sxU{I!meozdBA+CMx$I#RtUX%B2prJ}a6(7GX} z$J*5CvE$Ies=n9d<wN@%bYe@?$5eAgYm-yevy?)bl|AlE>`v$tlHU2oT>w^hTkas` z)AZ__%E)R@#hl9lfxeY0ei_c|dhQXusl}$WE4@Zp4*6{EK5+xC7dMYo8z<jXUE~-Y zEf25Yf9tO04^U~RhMFfX=F8B$*Jm|!;hwgNI(=3&yDG(6wC5<}rDkQjk?$@^8r^2b z=YuafcY6rFk1sZRNSC_H-k_=4O?xvsX}xZNvCGB8!Smx!pu0BI@QJ~41SXjRB*{S7 zL+DS+Z~F7DZst^y^s<9;WNu;r1;#c`35wnUW{t3(3cazL1<gVGdniN|7ZRGW9S36N z2xKD1C<`_R<iUd>DiG5Z*eURBP|rjQ!ZtPmJOvnLq+0=d9Uy`O4{)%R2w)380_n~Z zsW`XnsZ*mCEdQD|hyZ7rX#D`v!bhRg{?)+`L5B9CYTnhTkBvBq>lPR<H9F42iyA7- zaW&pGS(fx-V!+c)vUloK1UM)^x@Ukc95JZ+AA<@(gF2|pPR^Sa$yn5|{3uSPL6*MF zbfcqhGqkEg@^sRqZqtvAZ<44bY53Jo-EXdbz5v4%W`=*p^0k)-Szu<cp1dQ^-PSn5 z0lT*y>rdA5m1p@@CnqJeiVlg3c6#+*z46Wy4}e=;Vw*-w$=7ppugBcSyu2M9vzE>c zPh4*v<6TUDvJp+Axn(T%>Xm;Q3u4@#_@f~4w=vVYd9zdbP?yx&Z$1$9+fsRkIj}hS zRqV3q1|fPNR#%Rl1-7`*fTzfUM$xPhgG+-JBEzOKBMF5xGhV<k!bfS5|BO652}@!+ zN)2$Zaz<uvEVf#{h@GMU$O;NdBa=zU@EHrU8s0Yw9;v8EU}T1EVRl+ZwY;DZ5;~A4 zh(H8vC^cYAL_v;9XZd5!ZC_pw|JsQ7s)NsLPp7i(-$hAfN=E}g4NIGP%~UVzdYy^- zc5r@0K5M+H*>gbT(2FfINt-YtcnOUiT50>JYTW)OP%<%2zOT8+GqSR|kW5)iWd-2i znNgEZ?mt2N&F;vig~F?+lDDZZZaM~sH5_EFRI%6U6|{h1mH>xTCJvtS>iU(y4;+ID zJaIyLzCw0f8V8L%&=NkIWc{{mB)H8LcBnoLsD3YN=n{Iq)1l33pW}N$tG5lqzmZbr zowLuK%{+;MTT^{4Z3`xlaerPsPXY{kr^Bn*24*%hPHg5+_x>x_*3XHG1kgo*KXX<H zaf6wbpdfz)osRNFQ_P5_233}5EQx~?%>r^*u!o6i{ty@`tYluQVNN)U1zZ0xt`OSC zjAq7!$iikkoQnB17XxSwh<<QR27UrEC#9_|M?mvrWCRnT-lc#cTaeg!;Gh)HVGn>q zQ-Er>3atX9gUlIl_Uo6WiZ#1z<GtsSHpG$Y5#|8yUPp1?uBzp^Yb77t@Zi+VW`Qc0 zB4`WqoQzEdQVA}};=SQXqc96drG$#{E@~V$5kK-{nZC<h;M}A@`8l8V_#SJU4zG}B z-aeJq7o^AD0I)4)C4MQTdgRMOmeYo9e>k`7yN&9kL31bN0P9@(+%+z`R7vh=u`d5g z`6kEm$39F01+8^iYm7gfjg>#eUzFmmf#*nCDcTgVTiv~0bn<)IfJsu{xK#IwM2d`W ztKpRVl{K1dO}Z3~em&voO|nlGEbxRj$gakF`clQEy1e1l+~{??08ZIfc`sZ0{JZu; zz)uS6P#2kJ_@V~tkH}QyugQ&cs0<xZ!VnlqRA_4O8uomCiJ97Hh6p`QZC_fH91JNU z9nWr5FfIfRPCi1$bfH)M4yq8lr~6aJzgmu)Ph1#m53LztMC=CsO!>hlJZK7xCR%NI z*vcpQY7_AU=Aim|EDw~DSXh{}<$~xUh-pl^7>ghj%x%7EK$L2S+iupTvEexc3D+7; z=9{;W@OJ+HWZd<hHEUmdX7X0=AL-f{pS!8r;dN)F%1Y+=^+k8V_a(i8fm%t?Lo;xo z=zQ6G6BpP^%M>VR?Z{qZaa#Wg+*@CGwLyGgI0~5!Ol9)lMB=}lpLCNr7(cZ>uxnrB z7emNvv_|h7gP?5n;Zb~!oBfhr_~5yP-uLsVX3`F}Icx8xwq-370r=q~CiBKVRc;`T zTdGI$+Yb*f?(zZdGjC>2Hc92!S6}FE$c!HRSBzyKG1iupGEohI{pIBIN8q5yfQ+X| z^_Pt;LR)fwe6#s@(F#XH$=kq5x4rl7LY;-N7|M8zkD%^%fi^S>PsWBK9Aj(tmpv0o ze8f;SNI=L$x?@Pujwbw({c7O!<v%P3=xI-ys#{&r+hx__!fE$#?`Z4!*zCY&!KT|T z*`}9-Z;6hnYjhpAzjfln#aDC@et(o_UOXrHH(z1T0Bmq<3byASkO4Dn_(H?Nxm+@Q zJ%=alFUVv;X)6Mn-E+J`2;bH9ulNaKs>c(4f;toJ1NsVUe4}Hl|KKWaH<xODaTS=_ zejr@M19FvSAF8^NcY!Tc4gS1U4qCww?!I{iENMBe3a?I1NjMhi3$!ggnB4Yp`2_%T zsUp%p^w%Ftmz?x2{l1>eumzZ*2rYs9;X=cYxo09Ut#zLIcUlwa^cue!{VT0iCmk9R zYfE=*+LiJxd10lpD!Sh>I(KM!|42%w*uW!Oi?<E4VqKw?RE<9KcLzpcZIYki;XFE4 zj?^YI9s;m730-P~waK!N_Wql?OH*?$8#29taWb54yWMdg!;3G)T~3FhnI8b}^>{O= z(G)q@=Yr#hFu>3Oj1fwTW=#+b;b^y91{_3Y&jh*@%8Os}3oI;v8Q=wf>JnzG*ta_M zgsR7vl1>g`!XoM2EG*#MpsJ!U9F7lc6dkrLW$T`D>+TTMJZIK?T#y8g$icB4_BdoD z0}>Sg*$5C;B(kSrba~nRq5AV{^qK^8U^z_Cm1RzYF&v6az`^-qA|2nNIuvN42N6YX z%a+d&Tatc(s?5JYRi=vVP)+{C-FW?_vBm@38jG2uE7v!-g<0RDzh%-Q6cFFqI162( z((!*wf7YKaZKXeJf$qwI==5>#ftH!sbq3ltoqfWK8f?<uT3m2&*ZpQ(wJYHl$yD1S znemk}+%{6&&>^-Zx&4i*&$dwYK)35DAF8`09|AvC-Z;Z`8#xD?`?eS;zn`pb_JN+M zxH#6*=K4GR82|>h>D0f|U*GWf>a`O3{oX2bcJqfT4l3o^kvQNq1_0rA$jEH9Fr5Jz zya=FC{H>p|uo1B&c7MGiL~Knu3t)ke3a2ix%)CL0AQVeYOZj8X-QdP}=A>sH2{0*8 z0~%pJD-qq`%ZBnZqMG}TU8B08^enJB9gbfBqZjuS@o@GNNs33bap(*<JbaW{5fR!r zk?J!7N5-KkJUr}ZmNSF~Ml^+mhUX4*5J0C9m@{t=SUieaS&`<NC~c}<QQu5@>LXZ{ z-sPfI{K>)RANA+9J3gKQTlHsob+t5UT*4ZuKVdGhl_J;~PTr|$8Xc~(ns^ldaFL;( zulj4#xB9=Mk^7&~`21hdcr(1=56zT=70lM+$N$eN0Wly<i@yw5@>g2?W5B=CqQm)@ zxk<|0(Z9jvKbdfN@%)wv3!w?$t_BUQ7FIh(-yi?feEiDa@R}GPM+YrfQ)Gt(<nJCI zP$c3(&U<7O!WYAn4)T0ySb`x%LI0vJu)-tnW2DipO^4C~gux3$D1*bg4@1a0q_K90 zCk7IlG&rbDbz4{Q&e;bUEp8p@d0lq5PMmE|><#jx{ITL+;|>%>L@Z5{3k_A|;X%h> z5kv)9#JmPZ7>2*aQaBwEB3Kq_dgRBPW|t}d#t8SS!<Y4%)tg%k^x1br7Oy(E#Xt|5 z{b8UJo9hSzP3c4!XcS^V=l9F3HBDm{%Sl_=QRZLS5tbhAtu<b45imOveu)2-9i^um z-p*G$jxOI>|CJrLs(y<(tBIocoE7^eonP6}Wh*=8dUVVokP2X>P?jX~8&Z>hL2Bf0 zNS#6;wfK_N+s0uej{hBlQ!qOqm^$YnKtM3H=QpPM{K8bp-oV>3zQes=U%5Jo3S*(2 zkSaUiuOc18<4dBvpwQ;jA7){@AdAN<9f2Yb4<51M1-X}i<cA6ZD8=1PxRa*xo-L6| zyR2Sc3Q>BWZ`2z)t`V(xOZ36LOmh1Z-a%V=I$=s^q(7_y0h}M8Ml)o4g~zG`5JjyB zg+>aB^C@fNE48j42|s$v3yxoCAfh~JDJ(ejmJW{Qf%*^+iUqLenj*6UaOD5@eE(6r zB0C_{sH6c})*L9CLxIWMk%OS>xgHI3RJEJxzhcv4D>iR#X+zuYr2Z>5-K|Rwi8pP< zChPCmgro(=X2I3pv03tXysFlyTIN&9qg3Z1*feQ8)w0%RGWT}%-AV5fBvYy_Ryp3h zz06Tl(izw}&;r?0a%=<Q9+@MB{c1A7jy=|<@4SLmo)z4i)@t~mr8)yAUPeE`fx6HS zn*r6lr#%X;^^7jAD!W#lz8C)4InLwqNAHH*X!&E2FlB!|c;+{0RX|8<SyJj&P1`W} zJPn~fCV!}pKGgC$U8b$}o<O9$bo$!*&PIr&aFfpiLj^b&8tTV_Y~g@e3AUf$j3WeB zKW1zWGY=llnezxzLzBZ)nJZ?rQ-$1nSDUN#`kC}6gdQ=c@$?i{YVg34xt)vdDQ~*8 zQoi<G!$OIVCLBgxPk4u{cq1sV0)Z2;hJTqC{Ic$mhA0&q<0+BR3NNFJFQc|L_rL*i zS_<67kQL<jyC#Xl@f;0e50RDQA*7)R%qSAvv7u{$%&9^NIb;iceFF-8V>qk4RpELJ ztV0D=^P>#X;3z;E9Bo?-jv1uE!GQm<YdYX3_&a?HXe^q;^f?_lWH>IL(q{2yw~6G@ zng3wmvj5M(GrtU6hh)#5mKjJXYf8ETJ2!4%Ed^4cfiL~jz~);9PWe9@xG%o=(jPvw z4T=`O_)sCD)sB@T%<ezNuIm99yMd4oi9tT3<(p^d55PV&ivfj>LI=eda0x@8mIHry zEgdn9n4?IGm-P*114L?)pCDIg=85j!7+o%-h4yi=?l5QT;}@dD6a*7=pfpAHr1-No zK5h^-h^vfor`OB0bm)oMYxOL!K909G3hqZBQd5BJd|T065Zp(E!I0r7JM(sajVO7w zX`58hzHcAljB7pImI3!W;$f)%YFp-{0|Ln}K}ZMqk|I^MHbLe9<}^laAa_*v3>+&5 zin1)iP~?RzODmGP^5M&3T?NKJL8B!sUSJ<9{18GDVavd|-(;(0P=>S&)?crE+3FZH zxcazzroWqSvyyg<5C43<+nqF4wBEsL=s8&cOFpKBPcPT*m2I$jUA>wt{ysl$Y_0q1 zuJ4`~Ahs$eRJ(oKJb&n{GUQ0_=Uk+YK_zatIHJToLX<eVRGDL2m8W`>NqNjaDo-i( zPpQ$5Q=&gZFlh1Y{R!Bfc<t~uxVvODUD)#7<@PtvWZ0a6Vos9ddr^^#!^?=P+<=P5 zX~!RjI+gX?y`I6VK^h=#Qt>f>>P^MC&-e8$*1Lt!TUHL(;dknm6svuK(lZMjvab#y zNQe@TY+T$5Vz}XEZvbSxzp59x3|N^)Dq)U7c+L^C#6|O8`Z}L<Olg<(DO;637TW~K zZ*e?;7R8FAkZe%35;37%Ftd6PE_PiK>u%k4fZmzzt)E~T+802Oj#^QmCICEp1_kn0 z^=QtBcNHJ3TduAyod{)vg(*4?htC0ukQJdRzj-hmYA^*Mcu^`izW_H|$s#%sN`<0J zw75frRCt0h$^r@(EtiG;&R$dKH8kZP8cCUb^5qrxHm>E4NUuECqcb|j5*P0^oyq$8 zF+`86<U~SB-OVnS^Buc`niZt>kzKUtz_##};H6g_TjJ1GYQ8s~((KkWGPHb9G$rQb z<f4YD<&Wa$Fuw~JPaZ0lyapAA$PX?9W(DU`rw!k0J)5OKfoap)YpihPqL<D7>a$Dc zFf$jz%xv%x$;_`Vc{dcWNkei4TdD4;ajAdU@s<SZ{ttGXxKjEra`kci0Ms1fWPDqC zr{wL|Xr9>bM-*4}x1QN1o800n;S(JBUsFD={3069Q<@sVm&V4r+6?{Sb9-(4y7e@_ z#(zQj?+JJ1dEVKdxZQhwz}ec|!T42t!Y%Go9{b5$-HG$Hd@j~(o#Ogdy`>(>`a8%S zWW|aP^L3gGVkUNC+BfhNj9p%$FB<6p29Vk51LP=MTeC96;Fdk@AZ9GwE|ljB@D~1z z6rLAE!iVe~nub*o9X8JU3}8-z;|$DHyNQ5wfykj6WomXdfl+nl_@{j~e9r1-)PNZa zyfoPstBh{IV?j<Dl#7^*f;o>nEGGOgp4WYIA&&w#B;;&ohP#^6gaOE<{DFANF&6x8 zY6WK6kdQ6e36OW;)6w~y^&up<yOt!ZL18>n|LBCB?_D*G<J0#|s4VSjOc&RDs+N+I z-y|iqyfj(VlSyS+jrQS?a^OBwRn@SXzkco1cdgUet?yUu=3By_Erv}LJU<p)B=E_+ zZM64LpiJxmfkW|yc3vWL*Cm&Gx@5@Gx${-mBHaQGc$6Lf3HqT9to*<O7RyQ59ztUR z(Q>fc=uR4i>IbZ_nuc2zY~u?cKR;0M7eD7is^#DO+()G9M;EM?AK(6^1H0?P4k51( z6_O&F5e}fUvT~jaiXXWvHh<{dmiS@)PkL9FWb&_yxfP*zIYV>G;?`yKZ7rh3Hp6_H zvE~A@bGbH;M}34g6kNS_K?;yE@e@G#qr%2{*hA|`r$=<NaiQI}x|$Ag(aY}ymPkGi zkF<SOj&GbuTv*hfD4Tab<&f-^`~;beyi)Ec`Q(RBRok-`0keBGPd8Za_Hf60l_aTu zYJZdcwAM60)aJA28G-D>W!SH#ykF`spw8$7=ivC^hV^1>uP%0PkOB_{*$2u@c>P)l zvIrFlYpg$eXby!{j}t1!w75OOVdW<eY3}765ix^11tC`?3gH0(hF*S@O@-^;?6^+& zD~{N(_>n=$bjI?{pt)cZ?^&UmVF2XR;9(%1U6p=cWwvLJ7t579!m+Rzz#r9CL3A#N zI8hiKz{d5vML>wbVlyG+++&8E1In3X1$3^WMo=0pAO)tgaS^e#b!T{QAMLheil(<E zz)}A<rmu|nGrsmKjOadcuc;Sj<DR(Krx|V7tYy1Hhf7GR^+#Wog^9^&=^uOS+<41- zT1`$LORWjN7TldJDfSb{%wGSP5}VTR(=g}J!*NLD>4Szh#bO;2dE><b<V~Bg1>@4H z@x)eh+?xB<3Vl63*1Jt3UAr3N)9EG`RWVf#`dh%VbYY{tuPU_a`Ry%H?fLJr0TI=> zf5`k*Hf)J%u~6;$0yS0ol*eM+lXr^a+Zv&Y;$NxNq*d<nkF0b+3udL#gTJ#9oNZw% z-S{6_N%_!=(aFUx<(1LrxGEoYHXh+RP*UP?)2>%cBX^{w!uo1y`%aPg7NdB-h2(7- zaP&IwQ|?ro^P90*fieqskD^b@{k5MwT4%d7EEkQAUS49U5{3g|yGBPAG?P3y#`DLA zTvlzTrb`NY!X{2Nlzb1e*FPP&%jyb!P*1vl6YohTq57;{I(cFkNJqtFZ<!K<V9yVm z1IOxEw2QHdN-qKc9?e5d3%kms5W&ML*sU88v`?n~_9ZmjT?<8TIk=k+ZdHHAl~ksk z(?SnZ7AnA>cJefK;qnjRp%^+VHp@B!tA=p!g+cQGvvy_xVF|=@sUFU}_Smy~%cbDH zQKUr7q2Q?SFo(gW6CwqGEM2&LRFz2Ue=7%sQjj7Pxm^Ru$;*v|g(OhaV9%yzr)M9& z()hUDOyfj9+&^-g)g$(+K+dZZJ@<XNG)`t7ptCd$`*;g6b#T=~r-kn?)-uby*u2~h zZ1pJIV2{H8SC2w-)Z-`k_;)v{np^Q*Wp&c$p@z$+j!7BVwW?>;K%s^lzYEqhU8hRc z?*F5mB-B|sz9OkTE)8I#qNk;Bw!#EyRJ1N2jfyc9*r@1)jS8nWhkb7sHFa89TP0$y zs6F(gE54(sm1FVhL}#d#{!U$tMm`^wK2p0l2r<be*;^Onauk#r(_lLl4a>8wB7tIn z<3Y4h6f(aCqgx5(%uC-W6+ND*csJV)YO4WIxQ4?51VKCimxlK`^ebf^7o*T(7y~HB ziod!~A3e^s`-p#<IouWLpD%RBl4<C&ApH6A^&RqC3e=fkk#Mnp>X2xvqLCF{3?9iL z_`ibPmyz^GW<$1kqkXq1q+c2Z)(LPoB0*PC5w3t@C%dl-wZdBq2~s^F^j2E^(BnoX z6PY8I5&5}bk*VYHlZDk{D0-Gd(evm(M9)K_T=6b?)iX#jcf06N|BA%H*s1mYtGD8U zX89QOjuaj14rDcY>}3DBr8H1P;OyMXZy!!fZ!W?vMD;xn*oD~Qf=>?K`iyYF=kL#s z{t~k#x5Vt^Lr~1tPIo$4Ayw}nA6a1dif?<Qgpz!gg)t6IWoj^?4X9r*gI*dwU)pq> z84E4g!c{69oFMZBD6~Hr_I)^L{b4)d5x`S8njeW17PYd&b~_~$K{^e$`eER<WeAw8 zdwWlXi92L!C+?G@MBsgi1<mf$gtyz%UWD!15y#{k`ic<0KMmPnd{Uu~uK49cIykr( zt;%F+wpXvxqAjs4qd<fj#RKmRfsREZy;jKa06F4U`UXdtc{y`pWYO82mboCO+;?k> zJ)G#sBJRl5b!CU~143|}J93~-23b4@Vehq9<d)Jnrm5ugfu|J?e3Ivf^p&LC-#o~< z(k@=u7GBqKfI({__MTb%?fTiDV9%}A@$Sm(1xSEzw9n2J&Dn%4FDgw~l?Fsy(zgAg ztaz?AQ|RQ)o0nRwclGk0;lcxH?kiVsw^eoh1mY&A74%{)^=s<ay^nRa#Yex&{<@l3 zHn@6XJ%1vlb^1n=$F1U}hXEBv-d%prEXk?VtFv&pYjnj+(yHLo>iYSgU|+Ika&q^w zG`(J*Yh>P?#y+cge|&k?KrYAUf@cY@<tLLAlq^53IW~R_O$mLtcsehYC1Bf+fsXjm zb?nlKWOnN^>p8dmDY=ZjTBGx!`{^|HUo<z<-7EC%tlU#RCaDRX(oj7ihTTg^r>&g% zZiwM&=BY-elN%mSc6v1TMJ1V3WSp{#f4OUP*eN$##AZeO>yU}v;G?<Lk?-d!ZU|Rc zs%0eZ3KQ%xwBfF8SXVyUHrl$TY{h<nn<03wZRn`weGM&P%O!cMQ(;aGDlh<9L<wQA zcZjC!!`D876JQ*s2nU1>sB{Gj9FzdkWC1m7{K8fTE*;*>0n35DgmNbIXZA`b9ye^X zdi@v1%K8F0$@42?KY06LH&6+^1xPy)#{vL))*EdkmIv=@zztGQvuxuozpUII`zqub zr)^S6Na>ifRc?ia?(5zgf)rS-!0~&yn-Ni75zP+847VERV90Qum5NgbOQS+dgtx2! z?C@K|h9n;3HR-TVfV8c+rf5I_j#7lP_o5=fs)6@9zvbRF41JYkJ#a1T_D|sE>kwP^ z@Xp+j(I)FA+3TZ&<-%!O8SbX~!45kw(tK;*^v3;b;~ArmN>Uk|%~wi%a^V1TLdO+} zxXQ}t`M%}v&g&`E3yXF3##!?wSg@V&Mm3l3%;%2bw}&Jhr&_`t8q1FDpv@C__Dz!g z={n;E%gkQa*@bsjwv4;@dImgRE8Z7k$a1g&sM_dQB~g)T_WCZ3-6h5LN=H3;-*kt( zu`b?Snt%+`u036EbNW{MKC|#fYvPxgo^5j_H6sh5SK%nFb=bS-SADn@lNuVU4B5og zzD>HfHdI>lwoUel-EAH{-*uEFRKaYYsBU_OT`%r~|8c>%%fm{-7<#!(%Uz_szUa@i z0DVao3ITG5b9`wL^zOuhyih;5(GhO@@YBPE2xkCjSVbj}1^bd1@GK+jgH(*yahsBu z7C&kj^|03rv|-~MZss0Ngb1F0Tr2>Mt$D#rRh-%5rUqZ&U^+1h>0qvXh(sbEUuodI z__F$T+cHI?o%SW<CJQ+nvK3i{F#>G&Rr96Bf?s(Riwti-F#z6=0cz~%5HrFaos1YP ziW#|$1;lS^-Oiv~*t-On7u21HbM}z<)07V=Y1c|D2nP%H6vuu=#wsrvhuS^7(=5sD zuo6&VfBMAAhcQ$B9l4u}JG!{1Jn0rsB<roN@xe_~&#SaFAFkQX_m208ZIn7JZ?2&i zE}j2)^0YuZp^ETz-s$KQ5yPgM{*2YJ-J#Nf*^3d7M)-EE#aBNG%ghTg=@aQ;xN>f# z$fr<%;YPxk(`sC$apZ#G(v_!sdi(nI?@UWB&Q|qLixfCyEfgNyf9&Rq=ia;FrWpIh zwl1mEZL)QP9b_-Tp8NxRCl1l|&c7DR&#JL&+j;#Z2VK4t6BoJDH&+2#22yE&VSL1u zCL4=J!V?z)?6AxPXfHX)jnKYm*tI7U=>yE*EgsU4PR9$1E`<JzGtwc~j`ud&U4VB2 zfOR}$1T7+~hN5T+RP&}fFO|q&G(=<B09Y#>7?&cHv?e|qI^F_rq5*d=1M{O<!NNuK z>eKUk1*|4?)9u4bMjls%yfQ8^D#5~lhW98y>Uw0K>eeI<vUTfMuM(Px7@{qQhm*;- zFFsY;*=Z!yFs*)rw<|H4#_)}To&q4CGsM#v-$!1q<Mo>0r^_p)<1&0>Q8SWgp$i=c z+yUkOqUZ@gXUY6p?S!>9tnox2rn}P(Hvd1$&O4s%HT?VOIGw5!MR7{eR#AJEw5P_g zTYD>t60vuTPHN_;y-z7hklG^>r}lOtC5XMnCPqTi=hpN3J-_GQ=dbE3>8tVmeskZ~ z{kcA$_vLWQWZz+GPEGK_GCnB4^la_{Pafy4Yn7l>hNe-kVI?rP1)hadGqq!HV#W+v zJ!4;nR>af&8E(xTOSPG{!Xp*sA)?pg2No23_875r!zz-6OR2L@FO0o4KW$eM;UOlL z5^()G*J>m(yTJ|P(2v~M*@$_5)Zvwpog-#pS`f;cmMfhh<Ae{6k8HED8?&-;C83R! zE*mjlpQ40f)96m3j#Fwf-DKj(h;@GB;QCe|+==8?G>IQ7YTJTkOZ)FQ7KV*`(_*|F zOkA2O8<@nqnUyQW3{OISK#q@0K%V<O?M-anv)`X^-hB4=Y2MdTz=HEy`%gaJGMB$0 zZ`n`X0o}?b&@4I4`}fJ>(;#JX?l+4rnY*y3OJ6F%y|x8sg1@i6IrrNcps|AEyII`> zg72u5w)W@KFHZdq;d!ojxo)fOHVouU+ic1%#xFRs0(y#kQbx~qGqc=ST=YnWAzHy) zN;{pGV^2am{dS3jLCw>Ml)I0AT!36ifk2KAS0|3YvA+^3$9X+SCkrI<H{RD5BKaX= z@el6Qx>P)s1yro_V=j^3QeU7&zjUWzq6JV%J%WYrn*}9J-<Cs8Swd#Zfq`EvsbT%! z5f&Tq>3~b@G?V!RQTQk4Iu~Py{OcEn-&Y;ief|0@2LFKb8RYbd*T4Vv<J`fU_kX7* z=`&xvcbt`VCdJ0YIY?a2`?fEzRc40kcC?ZR>}PEo<M~b#9YSKF`bY8+f)!H-)LZxA zvBPTz7^B67go5y!Yqg~fb?y7{HEJA_<h->Y$3jdA9jY8`jegfZI3VU$ih!yqcVQ$H z1VF}5=Wrfm{2Jc`UY&+d!usHH$z^Cmr<=vL?1O(uYNy+BJ}f+rNZ}(@<?O@E{sQ;x z!YS|-0gJ^sF&Cy=6!9kesBg96LT8xD^?tP4n*Mh3Gsx6DKJmxL4nCl-|NZ2`iC-NP z2;Z-R%;URHZeIQNJA~tdr!h>m3lLYposw=H%`a6ZNJQA?{cjz2wUbw?`03m30nmRY z@s>CZ&-F?;bVNVo`~5BGu0d$wV+So~)ZeGrZ*XUHCp5E?_#TM8yyffZfRP9*?$skP zOWWMP0{Q$n@-^@~9EXosuK<w8$N$vW^Sr+wTeeQH{Ra|w7u%jA@jfYWhhQZz99EZH zxfgpze66IWG(|(_qk5gH@X@*w(saz-M{T;)?t8nJwC+N+VCycU83^pu75c}YkZ-g` zu8=F#VBdoVQm=N-%@LFI%q!1I=a%YJUM0-7iH7IF4ww-o9$RAmv5a;set$tA9*W8* zI2JF$FMIOWS=4gOXUH)bK5ldSqe;_fd1uDuqm5|Uu!1!;+w;dFcX5#9Mvc|Is-}y3 zNb5WgI>Oem9*dSK+@LsT_>sz1enM=U7&DE-$V29uE#lNX-S?pTqWi;ni=3}$Q<c0O zz8Ty}b3+G(&vy0|6Px~;`6J&ME@?-XpT5R=h>TVYj}@t=Ix^dxfqlL7hsfWS$J-nP zA^>@D=4z{0JhPl}VS?r@**x8^-9J)v-NV5d?+5mDXZV8`0PgTW;?KxakcYpX3WyZg zeWfmZxT(GQKr9KH^vdx{Yq#n+<|S5a6MTM4sZNIC<(v$7(t2@!XZ}&7F%HkHwU+Bk zLY4NcI?Xl>&01SUR41Xs>XZGhJJd!r*a?(sOKY_{%DVp%QZb(rd}UEr|BO3Ofx#ot z0{UU#m<;)<0%5rV&Jb~re?R&4@hMh7W4#Z#dry*AOAduaU$=K{avqK;3lSKuuxM6% zo_6WKCFC@mS1ZNC)cg5+ur8F?_a3C>_m&x7ho{(QT>*?K_7mcLQk@ldi?lPc9UVf# zxbv4Ol^}=)x%!ufZoJe;hEze%pzycZhtFPvs^QqZ`9I6YV_@<Ye+(?1pl1iHW)Sd* zsi>Trs@rt%u@lWq6k!5vw2(K$EBodmzlDBA8iby{I<Ym&vBL93HfAS%G}P2JJuRE9 z!56t>HQv(_?Ssjkcz&e8@D628;}Mimn;Vqc0wHk4Nu-NrbJ=Q8_L;hT57uz|%b&Gs zIRx07ZIsS1{osonaR;_;;agFcs=}`AiVFE6%U#6#%AvUaY_pL2wMWAtMm}^twOr#O z#Oge)bi=4$_k(56uGNcwfUPO@vQPbjVfXl?i}!&5{=278)F{-{x86}Qf_}S9b1Epw zEv(C-`ib%n5#z5m;y$<EJ*awY`R8vCi2Pq#1$t+6zA%-3y;pQD<BS%^?fA2&#YMiJ z|M4ci@RU%|1;~k_lg_WDpGckbVLMTDQ{0&;?Jswdh=Y5ZS#W40dbxdGmF_`e5*`-~ zMeBTA?p6+Y$INYcvA58B7LLO_pncs8+#p%YAuKu9YZ4wYQZrhbKZninoHD2gQ#Nop z|Kl`SMI8I*eQB`A+^l0e>BDs9v#}lkP_VE+he)%W1a&?*w;#vOAYY#WgW;Ly<(hE` z8d`EI9h(lr7>mJP_I#HJn};2p;JJF3S$I=BRmE(`YNW`Obi-XkJ8i+VjQ9AEX=2h^ zZSp?V#TdCP-O91B?mrY)e!J8@>tpAQPT<3Zs6J_R6o6I8!gN!M%x&6Ubnt?`>$ogD z0cr=(*30<|JS6vD0M-y-mpuOj+6CYhjTl7s85h*~SLQqcvP7Ea^CQuxOGNf0l+JCM z5C_){<IDK>##3s+CM~k&I`-|ZAyTH5HhpH+aqN95j8AACbg{tJ^{cZ&H&P?A&$&nl zh+f*z&RbLLPt>v_sBBE)mzFD{G^dij&dMht+pF!G5H>__kO_%5(2e$$e3iOj#uFqx zwX2%%-lhAve6ZfZC#=qWdfKwiRd$#K5g@HvNk5Mhow3kGBa7Vpi~TFzMs9o=4dSLp zAdRG@Oa?{L3g5%t?y9_sT~CZ`QTBTpRh3VWP<o&=iDcO9F{7zSpN+BLE952J*_XTR zD8fvjAc4H;A|Wk$c3p!`^jiPxC!}TgC&XztME~Prn~Ztyu=}DsYHl{);<8eIIVbmy zvdxF?^Oh@M%Xx<l)LhmoJN$!^JYfF`8>qf4-Y0YYj78bEc&74R^_$ablDyYN<jm~f zcgGsjx{u$gR*XyNI{XPSp`^Y<@5T?**>BM8&gcedF(u|+P*A<vE`+tGq`ug&`b5*` z?bt=CCwVjusTa+6b4rOdm9JREf&bg&p*<2Sa{b;$u-%#GV_5$GdH=l);+H_Ja^oB= z5mUr~jpuZI>*qw9=t_IHjTM>)^*IF=I}B^|Zd5j2RRcGWS-=Xh$W-s)+i#f^Y~Ua4 zwT8~XX2tBAcX0=(G%!2-yL204`E~GN->s~jA-gbB8zJK_wOY0Fo|IG-t#5v`_B`>T z<6orIR0s=-%itMG{23F>wJi~*SUn3f*bw3AwZJ3^0hW%`C;{bPCiD}}zug9gh1bWO zZLs^E_*H*FUQ5}tHw{JSbV|AWL7}F@eCu*k)uq0t^P1R1r9}v|JbgR%&SJd$E-^6% zERJzZOa>QTLs%cb_5a{y_hk;AYe_=I)pPvu8a(r0Wjxm*ThKvta39K+cCK)d9RV$v zD<{5!jTfAepG6)UrceBjZvFc4+t+tHAHG(BsN7Vo>VF4B+}dwK`<QAL>w-8v0`1kI zEIxe*Y7%s~c%zc>iOv+e)eQF4OWVD<bqF_c4$u?rB0Y%QD%E-TiJqy;_jUaUaSwX7 zgg{|v`yr$7uc|TR4};xB7~X>O{<59UsVhca>;d{WGc<Fl6%~i{(wKp&!=8KIq=J24 z$;KO%k6bHq2mfpcP2LL}=kv?o0VoBDUa`f0=HbjqoV%KD@@+E2GiuNA=LLbQ#Kcc( zC>&M7*Uz~o3By{Dn8Kfs36omJxub$7cy9+nz@|Ue&R6ILlC4Pk6Y<lQys`X_PTx8k zhMJDCW|vXk8kb#Z9w$h~A50W)9b_OoJ1Y<D_5zPec#uewt^RdN^KiiBs~bghqW?NK zm*@MR5GG!aalP6}M^i$9yfd|`TRFh%@*nIPy;pgez7?^{u_77*3w)3Xu&DSW)lpcw zud;n9F>=3;$BwN`0mFaq0h@t!3?UlITF>CyDA1U6+n(*+vi`b^jtW#V-hXP=<&O`E zt%H)YmAx$|bG3Hi)+o-PiuL-TZwBV%m^A4AC$SpUah~%g<Wa|&159i$7Ou+(z1C&d zX)^RJbRf1&iR(x^jj`0cI@m?5Sj(st!sySX;XWp{xI9dG+2*@>_1q93+iAxcbT7lP z=rKfwVM?w3zTUhGrS$TYCqaLW@DHh?T4<Omz2gI0O$~lUwHw<8A8RWlwwm#0_Dk@w zj&P>r(MF?E>ty;!3>$yUt@n*}Y4P;rfnIZYRoa1LXL+Wsc7w;lN{z-U!d`o~3uUur zEIG@+CDC797tyk(xn443CpH>&8<wB1l-RwxmRUE(nS^9xZa4o4sY+W4pH2!Vpx)Ft zl1%B@N)rwx!Fxf}KEsjwi%ssX;#QRu$4<$XX-aDayGQr$%Iq%iLx;6`?dDUgJuz}! zLVu#ebd`fS{A)(`+RS`6@*C^j532&4#*NfWi|!M5>>bebAN_w|IlYf=Z|NEmcy3_2 zT~~@kp2f&GO<$A@-MIT{oYOl<aKX4}IXha3+4szHHEDfktGGRWeyA4uR`cB>dIM}F z!%Tq3w}b+iY)MjML4-?q5$k||QVdF^C%y0Hi};!IUzn<^(|oJyO$3?cpt7o*vefl( zd~n{lH5J~t{u5Gp7`g(oOgYw~M|3DTysdNpV1(6f?g`OH&!hZu8tfmcTM-}+aDAf* z`Byq9WN0kwXk5|JOFq~3$UNlBpQT3d@X7D1Rx5pP%Vo1>>OAyt8Z`y@S<#k2j(Ve& zHV>psYsJ^);#s+E*Y0lS<!GPitvLWgnvgme)84`Rg>DnL%#Fz)J?^%FQS0k(N%ZmD z#}D{0e)te(!6U>~k6Q{(3kci`F8?=XvGXC#bH25ucTAjBnne${myzhPn6ubJD$qvD z7CV^0NoJ~W*|2s-#QJ0~xP=ceDzeoKw>!k*!)3z0<63g3t8Yf|zx#g*<lZ>CCV$}R zxisPIg262#Yt&p@v$TrJ+ik-#u19KGnmsB-<s9<jQo4UEBMM95<P3R!#pLnsETShp zdoSw-WepetldEnWUovjj$=bCV)eIUx(q)!-Q&-!<hhW7)yNvYXrY*8XD`lFa%7Lt6 zIldwpXzn?ax$yjqwXy4($ii4oaa&Vbkwr<3#tehGgni#>nN;oDihx?Rx7TDBWyZT5 z;fjMw)|0vHAH8Z+Ylx@v274S|GO8nrOg9^`ve@^1WRZ(xdf!-ICUd|)0Bihn(wd1X ztq-1<Tk;H*0Fz{S&%#XB_Do-uR)$31NRbnW{NrdYQL46HOonZZnhw^XZaz~lZ06d; zl6>dAN}~i45~cpoxR#+m=u@Z2+EA%P7voqqw<9;qK>q|%baTDKUfNGcQrywv5f36D zOy@WDn!)V&RHlocb&RB^&vm669E7YL9I1_W7O#|)uvTHOM?|T;q$-Ih=IofH4dH5g zv=OttLoyi_K|!^;Y=%*Z2I6<40&Ck^S7m4=lKw|npOOmeYJ3V6>nhz@<>6Le5kZy8 zIpDd2%ZJ6i{QyFf-@YEBJ%MH$_2Myb&4DxJ-w-}<0)^<ks=F0emn(~5&(Rp_lr}-d zRQCWN_RVJ}9>0BfB0igER2pBj*4i;Pa6{^3>hF*Xr+=I;<hL-AuuOew<`!{Wll8Fw z3SNPI2?B|H{Ws*svp2{29`Z3v7ZRBYx$s>7-05eR|Kg~@9k)(oV8jgs$~jze8|F3@ zMcn2NKUp_!z5jH2p-;-Zb0ge!)=Fm4fVO|cLFDm^LRr<B^!XK=AI9<IeqP$5XfaBs z=T-L^Hl&BG<z{p6f&FWgTCWlYGtbo}Z!80AvW2$mCkF;|dQH0$VPl*3#-xv;g_5e~ zjPK8r2fYrE7}r{?L&dmgxKU?b4t@$0>Go{4_Lfne1EYfXSFLKX%-7)p)hSsAzM~tg z#bye5HCp{yHr4E=N~b@XzM9j0=P=rAfkl@~hi;dSx8`VSu30gDLUb)NybqE-uGMlt zxxXUW*F28aCgWZD!@Xr2zE4cxN$f^Mk&KKFdqKf+yQbf(Hr0&eKn2BJn9aUw#hl$) z&DW%0Wiu~HpA7gnnVu)}ls<tbX9;T5?B{k_^&22p`!f`K$hQb>N4@A=%v$n(!l39s z^`W;rcR#j)tcC7CfqaPP%4X1IZmZm~J&}>>bp)wxOlbd`p+?s<O1(=MVdiDVoveXJ zJB(RL4DZmAR#u2<Ito@*QSZVd%uuW;&&`fTXL1^^5($<-v#{y5+N)e3B+I3<ij2-s z5<Z25;Uz9cSbZ5nMN$%Pju`qj8IG{X&s8SeiPsJ!+-cFWH4?D(x_p0Q0*`jAR|vbX z{147j0@Z}!VJRSY%-0v_#^|r0Nl-kVGU2j{)W$6_N>QA(+z*<Ex-`5p5`NDp5d30W zTMejiiLiv!X2RjqZ({DSFo)&eLZ<KJapdiwP&0G$EApjhHA`!Et*a+1(iV0(t%4<2 z(d+BlZC72jRquJYy;bLK^y+S&vz`bpQHjOK$loAJ=KW}7(pT$t^-amp{!Af;SKEo1 z1~VzPpOQx-!h-`?7>S;Gn$v_hTsq9Z*HKu8=&U1r)7j0krqa#(e#NprY`7XK-Enly zLK|`L1K_r#o#u|g5bdxvok`NQLv9ui@a3-hw35B1X{2qTXvP?ATAO!i92Z69hYwIE zu8t=L$JhDY_a3-U)estlX5jQA7FH<hl_8~#;h{!F{Y$ICal>w&??G@#c3Mf&Zrc|< zkema(w?tv2uFy924%AvVcWB6*;2Vbxj#IlA#FpBNq@G(i7hU|5cseJ!B{sGyvW>6& z7QI9|qaT43^dxOrrOHV?qqFLWQB8%bt2R=?42dv5<?aVTM-RSCNUQjX^7zSi&$$@e z!^g$$Ih$d(-U(f16OVYFXyBR;Q#Y0>3Vm;F>2|Zy;er)RE~2HO0IML5;LnCPkt^Wo z1L|9edO`6aVj)cP^+=r~Yr~NKkpXu(5Di$anLMm-s(CYKvpkO_Rl~**);k7t?_i6& zj9utB4`y+`f3Jiwf~ZhPzO@}Yy*f_x+bA9_vsvGATwllBhb=tOxu*!#u;4kBF}De? zi310qBP;_dT~AT&5B)xIh^Ukfj!fKhrY~sjZ5OK#PogNsh1`vtAJAI9YHqFbo^r_~ z#fp*5;7Z^K_L(^&J!ttY(N{(1nwiEmv;6ElaV_OGep?3xn^_P^9o-z>KWF{1lC(`) zLhk#Di13_^l+!6_iZSX9A%&`G6Y8>)YHP+X&L`}ykA}W;8|31JHgfA>jfb;k5dsA! z^SzIqPdx*D^3hwaq;ksC4|4ng$ETwL1)>wlJSb~lrO_TWw*>HP4)cZ)QD<FM29`^& zE{sgC49Zj*m8B+ERJ=42aIELXjAp*6fwMI8iHLQLRSxNY8e(ZwzD{d5Zdy(lWUm2X z#MniN9<mHcHnt1n5T3u2qCv}{WX!TH!3ESSA1Ie=OEt&O;hz`OuaaC>qPKRIVg_;A z=uEcd=(>F8@tkx%*68Zx@nEmCK5oSc%k*SY4a(V-UQHwz_K!wqFX-yF9O)Qm|5KgX z<BKP325i-fDJH#h^BnWdV2RC4MW~b~*5YU7@Gl254bgobN%grGjJIK>3h+`yqYk6Z zIJ+I;SY89#W<*TYZp|eUUu}}>#kS7){dIb^CsMawZ~Cq!YB^eRwzT0AsfH9s&KKTy zXe>BvRE?3hLoQdt#-l=T)~MMHzuA#Q?VAK6e7K4~zREM>bDO7CfdJ*vgyo8K<d7H3 zN90!EEi*GA3D?fb`NC7P$wa0eLqNs-2lTQ*jss-*ft-CG^c3F%6Dh!oGl5tK(B8fR zs}K-6>$d2*aE{V3i<n-%yRToqd<_x!(h;kiYVj}uf6Po|T7acGUYU-U>Xr?=^X%<w z5a9Yn7`SupZ}8`pV;nd{9B5_FctwDN<Hqk-l6gP<!0t#JzQ-lo7;h-_yITCPmt$O* z3&e^@;2PWC^^)~tSQ(G3=6rQjW~^LvC;Z;e(#KiUHi6ITUbhb{6fw`7ou{@J;@RAN zX?hLYYk56cM`;S%vO7=gt2yuwa!?cv&2@bwJdD*<LmS!QuxNLS*plvscfFT#S1rFY zk%to2uac88u&a`KM?$Ke<Bn<tjztP3hxM*;m_al_ahIOyP`Ld_E*yQWt6TQkZF|fp z!kF6VF=N{a)3V=iqp@s-epo`P6)YX4+dfj5-`uGQQPg*1t6!?5aG{U^XVVewqx(g^ zqw^&uVneOd8!od4xu1J|Vi+hIlyF!t5??@2uc2jgc6T0TZIkS2dKpmXI=+SFp*9*W z>$Amj#e_~>*Q4~_w0Z=RwSPlQkDl61FAy7=JuHJB)hi*Pq!dsR{#cm<0Cpb7DnAY1 z%zYl|y}>K`F{R+7o<kH3f4VZ|$!9cNxvym98bK<8acwj<1fbGo?!ryV^10b8cNVu) z+|^K+lF{JW_VhjieuXD!vGa%5VR~%^=}>3dJBQjN?v7k-&f|9Nb0zJMT7}^Riu{at z?stw&l$XP47U@U%NP{DD`oj^DLVD1sXw0ywuUf%qzNcrYCjM^CXUfvN3jhS;g2&(p zKGyKMy$Pz$e#NY-ln64I>j9eL$o3`oK&K`KCp{p{En*&*?_&jffT%dbK8~ri>vSF$ z&rsBRyMmlmebgI*HA3Q(uRG?#9e4d72@g7Z-1KFhpARE2t!?*^>;{ca-}pP5Wy7%v z`bMDNI(Lh1uBgwj9-rTZPh^IGOC>>+obnNY&rZD94>4M-Gq1KCp01wN%&`>JMZEM^ z?<U+hxZ_FJO_!-0sGzj(dS@zx-zc`SBWEv>pQGN)&XaBW-_*Hs8d-R5x+cmzS!o{m zL{az8cO%q~u91sp%&M15J6dx^z1A|fOEw(Z$Am1erFm30;>#c1X*@r?6>zo2!t7SI zpfb5($ixqxo~PKD99K2I?j<(8VJDI(Y{x7f8>8eQDB{Mk;3Qiq%6d<MSCs417DizM z<Ee=4`;oom-Aw-C#k#njy`!0nKC2a)#L1OcIp-(i(Il6oLz}|x*;lygCGh1uhcn`* z?3?ildvyJ5cDwFz4xeDq4Xtgu5`Dvt<)(_$&C>Jy)1QZ0<eZY9>|AbiC6xf6ztpX^ zs?H7zOJ~jZcC_A*Q|)96Y5`ss(-v~sykOJzC&cowf!kbe#Ctrc&&X6nJcE>w-89l? zMvzGLW+_&-W!DcrsBT+D@N1U^wsn!Bv;+UmTc}9M(6EXVi&zg_Ob%Xl*hf0BdM8tp z#I<|f_Y=3t1WIzm5AO*;_54-Tczt#M3AwS*cuY2i_cpipG-KF!6}>0VUgTTW=8jor zD;`4REe`CMM#LWYb8o$ICDz-+_qLKAjTvCY7TJS5WMfrRi2=n23S69a_PqF+`L)88 zF^ut!H{IK1x_!Tpv1HTrYZKif$x`B@^%K(6J>Z*DZ^E<v)Y7D8M;<AD62lGGaEdQf zl?sQvJ=zG5oAHvbGzykKbesWg?fdXuP=LMrdvSyQJ=3m2NQqk0o~PX=G!^Zw7G9BD zzC&_C$RE@%kDIuxDy}uI*12box^%V-B35B97cznZ&53zOXYp-|t3*XA`g=j#cVf9h z8I-MddfqTG%1B$Z2=$p$C=|tCBtL2<d{t~*#B4CFr{zF?G1cU+j<pAMQafey7jx*B z)CCW|^uH@24CaKYkqDy8<2<PLhdUAv*NtZ^<wex<?BQ)@N<q)N^z8wNWX4i)*<4ka zJ#1#ZbaB55y<<^wD<^Hgd~Q@JZAW@>Jt@jV%5Dugw@;>jq<O2^GyegU?h=9*X{&nq zg}r%{(1S7)ay(M+xf$&9+%CN4i!njsvU#3!$(oyi{o?MvvA0yaPuEWfxnt8Y9YfbY znizB64b6wfheHF#D`3$Qo@<@&c?|em#y$mAnWa^`VGo5R9xUf1WDN0g$IKhMs_~pL zA5w-ch*!l*H_U1T5eGs)F3GF05v)5EcgE*7>(PRPZpwaYq3O6j_fqkUFS$1h?x}C9 zd%5g6mdz<Yio!H>jQKG|iAhJ<$JDf&U~+fhH9Bsn47;U&7Xq+)L)qGbER6DH*<kUi z=m`n85rX4Jaj+Mtn)~$NjZOW$ZW1%QwsG8-b1VHs=oOZn56cVw#TGqq^-&L;`3bpA z*`i{k`Uz0cGciZn2tG1$3nA*XufyZMHu<U$(GWQrtD_KDt2wwFwdrWsGEemJ;$l3U zrv{ljRSROiQ)+}U*FC;tF5bW3ZOz7Burfz1@lvJN&+Mj=>dmB9)69gqHaCU4-MU9= z5SuRXF~b!DtSXgb0d4AN4cSauNzsn=O<yzVwo#P^{t88sPbYf(rGqs*^s@p#{{g4u z>SQ@SqOvETT3<y-F0w^8{-!cD-&$EtPpqqu<#8)Gyh2`Iczomcm^UEr4mNZ!F$MDY zcL)d_f#$-A=dVE*``A{J&XZal<L6qcy72z)iMUS!f2(#TzF4pk@4DSC#N=SAESqQZ zzWZ(%Gr%I}+>CpC8glR1zb8Pm1@xlSpMhS30kEGzUf;No0-Pkr5d@JumecB<_oYOG zdaG<ivGX5-gQpD|gz`s1M6$aOirjUR+<M#cm9x5i+<iW!rqdy|x)b|ZQ$Hb3h?l=x zgpPW0>5Lp}jj$LNpQXA%S(5p3d3D}-(eo<g)J-G8^_1YDVl7+i&k6|Nu$A%|$M(#@ z0$Yl<dm8<V?-6`5-okulhAQ94-HHCb{iqFOLV6Gm)F%1Ka6lU{%Ag>takAtfP#nw= zpd5R+taJ>%T8VYlzt_lJjqBlxX<y&zj{;Wl^8$GMheV7iPiCB`muBc^X7M9&Dsy(u zt5!fQ;BHqKl5xQ6hQk`x<tS4xUOHG&P~3H<qD!UL?5psR8^xoD&vURZ6L^!YkihOv zjEwkEcUJYBj!`ddZudy-a>dIDg40H_6WYS4%w*Hew<K$^ZV#i#{_x06x65tXoD&5_ zsfpJOx(P0w_AOOh;C3VChX7z6<~I8K9Z(4M?RKmcQ?ZOFsCMpdxO(2E+z298t)q?A zLgL@Pt4*Wgw#{mC-rwakzpFU^gj`MV{jw)VUc*gy|Abtso*TyAuj4;3QyWA_RgVuQ z_lM}z?~ih0=CcOt;K8+9ei!VKUbxP-I>||`O8lnFa-b_p-wODr>n1)|*Ngy5HFIa} zPsr;|#tjBaX*}D7s-zw29ndH{H2KHD)g}ewBI=`DhGlKHZpQjsw;!MXS)$zuX5I0k z+pVMHadTbI+NwSFa?i^i-E&t=*WtC&wv)KrXV}^0nSd>np5Lr#nHH5xhM6?G-h1Wb zFkL6iL~dQi>1u?|jo?E^+WBRVL@w$VELA{bCtUrCk6BafThZS<`Jgs~Qlf8K$$=!L zk?jDW2k(Tq0*KrEl>5NyMAbYqiNn_=vq^J5)lXK1k*BF!JTl-7Hz7(N^sg?`s(U7c zHpBgTyTU~HM<n*{mKteJy>;0|yJ=}1aa+R3DafduFe9Ilk(vdo-5TCbQ<;_cA2Yra zaHq=s?lR(^CAMW#eV5<lo9h8MEd?XvnKU_W?n5^F!>Fiaw`nqkp}#)0N`V(M(8zQ& zu?SCtKJn*z7at&+UMq$QZ0U(4tQpky((KI%bD!i&E6|+0lQ+E;`5c_24m44|&A^>Z z(mmeH0*;`*+3Vqxzc#awcM8VbJB>0jG0hwq+8FMBKV(Y5gRE?44Pkz+;9LG#f+myB zEk+6@6MOPHX`>E=Nv?jGoV?FHqn1ohZ3ZeI?us|-(eUNvZ8qzQ5puef(lsK^35}<m zxC(SLmeBEWi>jat79L9P2Wi?cT)h}sskI@w;BeRfJ=>|6RG%-l*xgVa=zZ}odrjtb zH2Fqb=t+fP_wAiVIo4W?^_B~0m(nKxYnS3Cp6$GP^2*Cg<n8T5|D6t8vA;(qA;#n2 z<-k#X;0ihA<CY!5Rv_f^zX$$RGc$x3y@7fgjw$SOGp_l2pvv%<O&^6zNX(E!oYwbV z>}`w`C2QC17?A4@E9(mm0)iaV2WzyYY!y~1@<u7Rv3?&b741LgN<{K+R~MRj0?zYv zGhE%D-AK=7_wb@$*5LTfew0>F`yOI;`paZ*=#5H6t5lrN8u9~%+fNVAHVD1BhRA+H ztdmKk)>XHaw4e2OOIW+&JxmuzM~B{sbt@sa<^YxQDc*OX8vT7>$x*=(d#QuZqQW6A z^0jKyXTbNl``fXl6EK?IoIDQFLsU-#t?!>-BrVXdKiP3b^~Qx0y~rMtDyD{w&kG!! zMzqAdR3(J2%qE5b27T#06FU$eoAm@x$1ve?k?VT)1|PwozwZ@y-=})2;?6v0@+>|1 zqC2Lg4Ga+ER^|Nw?1slqO|UZG_<H4d>cI`r@P7Oer2KN!j|~x7Pl0*bMr353MZ>Z= z)p6f{R5VQg&wUzKXq^P@kx~_u^pS2=-Iv+ha2d0>p-|zIoXhJ$&rYW@3uU{kH>Q_O zwMh~?x5s38ZfLGF<AZW0BoD3nUp9o`qqC>VL|gizayB-IO75HTZLuru3E2A{(Noj> z#V;#A1*JJQiY0%@fGYX1jtl1o_xuw`8MO0}nYF-qL$AwbSs~lYomUSqjRLh5s$WTQ zKHhruCDj@%?Sy-ZS92<XZ!^jVeU&}??2=_N9PKh%<}S47u@cQ`B~Dm`#%w8Ma^za_ zmQ6E@`gUR$2s31E7dP%%ST3y88rzd>Ss3bLPTcB#+O?uUmyA@SHag}FNCu*1vbP(m zA5M8)7m4ew=^tB9@deb;V7-F`e2q$3VE@V`b5xpE$R|2;ZRTu5z+#3}XxCa2Bc@uV zG3*}WY12v^Whm6R3SU633_;432J)q*ct7kjS_(VwEBx-8Oz(sk?#H)p^R1@N!Jbwr z&Gdr8)@5^PRmWvCc_%<o(+bx?%bSp%6jpG&q%vu-1Gkv>&em!CL!NJpFyKwVDeBW2 zg0cEOm;%DwYAwchGJ@rng>zx#X6aRA2l9_Jl-1J5Aej-EUr+ao-nW8moht=@Ghb>x zV%FU(*fkC7uMSFGIn3>)wgLCeCf|GCTF`sB=lAFAdY!rVd`|b<OtXVNtba(iVpa@> zElro&xVbHl%NP*e3~ZEYS+>||QfRM157%J+o?^5IWu_!8e{a*Z$_y5hT@PjTaml9i zdlgT6+QIo|xi;cBYkAJ_xIR}Ut;I+-8JXKX|KMTPSu+)vDI_5qCS9rQU^PF2I9;v% zHbYbLP>WO}sAK8136$Se|3{&%MM8=U5zW+&Ba8rk47*(4?6THC8mlbt8ka<Cty&7v z^A=rW0!yHx3+`1qy#V0#&U=}QS(*zr7KAC1+Vdq9EH-(CG8OQlm&;;UJh6Xx;riNU z*}j+PFQo1(2|yil4{I4+g86KNg0;s#o>qJq_+|e_s2|Ti;rW*hvj2OXJ4NEb(FLU3 zPUM#@>F!5{Not#|&<=k~SEaQ6^wFhp!1`o!5vBK50;{_&Q4QI=4`VC^j-j0=*OHec zsysFx?tFly{}=SxIF6rgc)No5sSx)gX5W5a&TEm&AVc<2<d`nFSK4&=xjOVBt!J&? zugqHTcK?3KLVF`)aXqxz+Vc9>L)LBi&_-*Sh^KLFnx$`{BxB0#cCNf%Gp8R^^;ZiY z!PUo&m_RJD9l+5C|J%dI{p#VnF{??5yrCRw<oWr)eL7)0m8!b5d)?bSj=H6O#=YA+ zjvVm0{rGB+exaR6s-_poW0zW1<m<GO@aZUAjwrlq1XX*2hfzjDjWyfV6df^_hTGrd z+n3*N_MCDwwG%!oW4SxReXZ0Z!Ma4UKi6W$)(p)a5xz9)vY*h@XXycC6@6Usg7|d4 zn5=iUjFaBlD$IM<%D|tNcU~ZX%eijw`cDX9Xo(P0!IRs)HPUJN<Ed{=VUcO`U#%w9 zEP#9V8Z4cl4h3r^!1Z2I{-^?iXuy&Fi!t&ag;13h_*vx2sOhhWzB6-fuYl`Y2zXsv zeif^-V3da)s9(Y8pRVb1pKm}?R5gT?&&|cW>I2hv;)Iz}UO)RdV0Xqu<IHuV=MQd~ zi8Vj?3_>=*T;&6_s>r*K-;#j^{y0Z*T+(X2KAp|m%<lA3iD}a4+J=&jVX4uI+fvh4 z(U{K0zm{k>d`E*a_RX`p+S7lq;~$+}%$(>C#;&t^t#nrXgy8zFC%UXwGRoHucH8RP zmzkBa&h{ql^|jY)zM9yQId>>Ij3rx1buaXWbSvQm0E?zvcr$3M1R8HaoY_Axy?C4b z`|cl38sZjdLHY_GGivLiA2s(`rnavldOfBYoVU@VC5>&!i^WUpjh$<h;Bg8S$Cjw@ zbPSOs=M`HC$M2UMAUfUOQcbrhCEO)A^aWgLA%=kBmPYvTef1K(xWP4hG+8HJ&byma zur5aDV!=uQXTS^%$+rzp-Hh$<Il9DyG*FA)>z@DW{^$s|cqyjodD<dt+CgghrzU*z zQgJ{b-hy~fRot56#|BC+x)!F&KM*ugs5IVQ)7J169r-Nz@Ul=b@$fm6uBd~Aj;GMM zbvY%~L?13&h0oW#A|(HYU1upA`ZB~NZY@>2=_^THqH=GjPpLH4xg{So=oI#*_0OVv zm%HD9tNVN%D((v}8kw~#$V_Fv_5pr>hJG!3PA&B~V(F_k=m=2!H6tXbwk4nYjzoQV z2DZ0qdWfp}Re(q-_~B0q#I5-07Idz_md#+3yYdquRGj@*=a{%K@%xA{^rT~_t4_Dy zsN(`{rJZO+ZXSG;<C*N-+ohv-sNG;bxQ+a>L{nbW?Xv*9MkGK^V1RIOBL3(e-6&82 z)SW>b$91RA=b+Y2zPHIPi}{CJp7%T3(@SYD3oM9I{H^S;h9SJzB$L~pm9~hy)y&Tm zq!7BYbNTZEuebDdi9jDG>flOmP`Gu|qOo_7@TO-&A4_F_TO-@VnDi=FJBLYKr7DfT z@46ZKLm<yT!3e3LtteHS?UfN{9hhvgVNF*EF*Ea70E^mT?FcaG1V{($Ezul;Leu|6 z8;)O{I#dvR<)ZKayTB@LER~CETXe|Erv|AF=6*mK!|kfJHx^t-`?_|^j;=TTZ&q~n zR7>K2D=rW$Xu&=hudsKrHY~V8KwBQSz^!B^f(dT!j{Iisq%G2+qT4(8P6pOxjutNO zXI`-uyZHS3G=pQ0KCluWeyTqrco^ZBToH|d!U;PzcN;(GsM!-7<?qkczYh)RomlDU zX<Hj_7rfwFT=Xh2(WDYCyER8pT{kST^iA8JM#Y#^ceU)>&n8meO}J$(LWfa|&po<& zlMM98fSN9zkv(3)e7JG;Cxm{F(Hgp|C0Muth~le_yQ4sHtpFms;xw=o>h1~sgcRu> zf<YZ*dgJ%dmCDe9eO<cO1P>K}lWZGlxs3i$TBF-0^w%qflmE}Zg09K@grN5Rd*#(S zX>YflXd%N?`0|hZd9wuDO8BeE-aiSL<O(<YMOoLuRwBB(ESwE4tdQ=^aLq1_2EFRg zai)Nf!A6qkzh_-$v+Nen9`vIBBTsGs@?>bPpZVOffj>ZWtxDUkridqfU>v8Gt9;qU zFI5~^4>`gK*hs1zb12qLTJa}jAL_;6pe<#p4cEjG=rn4B>g6|W-K8c!A#h^?vMai9 z^1SQf(lFxc!PSX&ukft>Olm8G4aVTlHst29ID{>V!gA<C+u`Z$alP_q2UiabEk~;Q zbX7KfnA6nJUBOr-4;;=dIg_2>CfLA*iVZN%kkU}79{0_bHt6G^{n$M!kRb}~8ilGo z#ltD1P-CrjN2nvlX!T{KWRFDnrPZ!N(M(&<4BOf|^*cDW&lEicvl2xl{$RI$skEl= zYP5Arr2@H)_jc2$mq_BmTBKqf@-nCR=Ny_?{G+TY=m=5<h0i^#nA;u;eFC<KL98{O zLG@FC9>Zf?+~d2zX#3BJ<Ne|Fwc~klU{>~j!Up8zF*xBIsC<F40ow4TpbbAUa2kU5 z<$2V-)O?ojR_RT$%DE!QNi8trcP^Se;VFAEZ=`rLQ~6!UNivv~6BYaA`*BilFhlc9 zwENd5Ad~}oBM?Af_%*WeF4z<R^1saua8N2ec>t!=0L$4~-#F)|5UR2Cbk7F88=L#N zo!~^bm^8~SDx+!syOyltnY@($6LM0qGpjx7>gdtj;)Xc9BHLn7I3yum_ELOmvd^C_ z%F?+!X-=td*d+5ApQP)TZMTVmK(tuvS_VD)7to54We-gg=*aXI7gA%ADy6I&jJTDp zk3ZP!IWVu<MW7vGcSuwWVsEXa-a4n_XbIYcM8~7HVQDxk>5caC2M1P$E=KU+3Qo6o zVMgBLL#-tok<@R<DYGNTZh~Z>)ZYFGWmlMiO^GGr&_Qm{dBwWKxbsfIar?4AAv%2= zN18_P#f>Ag{2;_<)Vthm_{UBEm@BeTnq?ndD9hKGKt_H$^pvz=bmJ43Bq84*&g(=p zcL8HYVf=cgPxinq)uq-op5pqAw5r{`58I4&avoz}cV0=PgdMNsekgA->m8ukDi!#u z4ySf6{286AV%qpamCei(@3u&rOKf>6u!Fu?ZQg{)quZ^njYT_7Tgg^Cq9Gll7@uO( z`fUk;uQ5^YqRm|4P@Sl!vt-1gRcRk}H8@j7*#%^VF0-LhJ2eZ#+M=GC3;s41^H-Y{ zcAmA7rf1cV9`a|V@t>fjv=CT>-41shgZ}dpz%)Y*vtmINofXnp|7bFEi=Md2_a@nO zuHvt^mOI^j&Fvi@%oi3iawd&f<>3g?51$BIe4m#y&ZWd8QA4Ae$QH{HnLgB?kasvg zu&aOg1~*!l3#@(ewW;<{KLeYBe!myMxl)8H4B(K|AqCnjQmEs0x)bnwjb}M4tz((C z?TY(jLQ#WR$jurGY@XWMFwS;MOWuPtFIQ(mY~6XD>u5Rxby1el|Dva~oaz(P9yl%o zOT>g}ZQsErOvEAak$AqY$qrm+Z-J<$P9!Ya=;6#;wQkYR4=gU8#_D!OFO8G-W@L7@ zL_QIAU8vn#WS?M82OsTFf`}t(rER#TjjLIPgWaUY&UDq<0+sxozhGX`oH%%$hb`4S z&_Uo4Pp0QLSJKW#6V}O9>Qkxa5-1${cwFo9@++oRcdbNCD*fw<2u~N2hCMgMj6@#` zeElsh;~F^%sYi}QY3t7szjj~eT267<o1B27sP!TBN;n>j?*7w-`9q_h5Etyx+L%{Q z5D*4A|KF;bVg;(|`(Xgh^lkVU4U;Cv{r|U2;?&qhYLR<`bXodKdz}MKP*W5Cv&Djv z+HX;a02WE%|6L?=gTW%1)&zT+p5o24wVfB0tZEQk^x#7j?V8p|(L}oP5r1qK4JEb} z5|MLYt?g&DTRL3$oo0~qb$#VkN{=i;Rq)z!mJ3|JYmK%e6qT^~U?nNacztol{(w7r zn}$oRMjM1CE>kimGP6>DLL?t$Md|4LOXfBAsCxxVtLP3RRSDY&n2%>x_4}*`J@Bq7 ztwU{uTwuYKLN87QT0+(4NLP1)9?>U}2K}8IS!W$)Cu|lN)9mx`LvO3nDxp<t&4u<k zM+Ktv-cYv86TFnK*`%P_=T~07-d9NBd<NaJ`m9fXi4`1gAT7Wl{+~A&(EW~w?0^*( za{3ti4W_Dszbv-z#Z<yD9-cClZ>~R6axLhf#p~S#Gcwp!FD2a;uqb=0>H*4dmeW8V zhJdu;iN9}lod@_53(v4HFdEWin-@T{!P5N8jLY%*+zEYPcsh<%eaMI|`ZUl$GAax# zNbsiY%?}-r;-UKn&xa^x0E+K8D`H;Uzi$gwa~+58n>$$IqzEsQThH|CyUfHxzch?L zPj3l9_uuS(d%%3g@`ub+Er)%2?2@~9@fXb5r1W-bsAsV`e;()iU;+iv+EznVk{Dg) zq;q;G4h3)-tD%iiZD<EIllkzD%K|&reup|6$SR^RtQpx*6$-qyA;4QJF;ug#1&nE; z>q|7HXgxQ@okRJ=D~~Q}lENP)pE{#`>*Gpcb&2P`PH)Y`UNsTQhCI#SFPKxvVp`O8 zq<CZux2>p=f9$ZL5hIvSgy`Gs=%u~~#2>lx4@L+DoaD_|i{M*2Pess|2M{4z;_hPQ zx7%b4&$sI;=cJCqYKqFBH$BXSmMh!84;^)`t(MHzP@2Q7jQAtTWQ&DQ&d;r|>{~t} zkJgAIlb0(BJo&8pCYD$IzXYPv-xJ;gFVf3OCmKOcNL7%zh`-;sq29UPM(9D%jo&lU z58`+$Bt8n@ucrEmjBYo2jtMfwOrkDdQVebv%Exkq*d95SwJgh2E&$e7C<;uO)FA#h zWfJvb+V;h)aa4qXQ+uH|(ZVQ#FE@|3>?+q^i|ByL1w?Q2(V1f{!enHx|I;riViai; zboMuq@kFG_r{59bcI&m6aQJjrSpC;h$#Jtq_U}%ufz-@`Q*gJU+RbP=X5Yg6k$xnM z?26Hute@7ssFWBYN%4GN>tT@wi+n}%C6~++UiByiBjD=2j@Oo`6Iw1wG7oNbZ&|+= z%qu$z7TwBn=tHU>I@r;$iosS}`^uU#b5sTFAgZ-Dhf=4iVc0$QNlAzTj~f>gEzPuw zpS*rKE_oO`@(xvT()YVprm$)A6q2VU$XwnR_z#kmbkWw&2(BxkGM&i{MXnf|`wMk# zPOa#Otl&a1*<=!HL~N6u_BEE_Al&41<;K>B?d8@!jr5mPRkNc`DDn;28>a;uiCC|( z>myqPjG)CPt%#L;rJjAr$?<oJ6QK(T=V-rkB_f4b7QALS&w0_d04}wH5bL5wzx7@J zTi|R+A+6D~%;S*h?_dQY5wj$2X4YfBW!gy5V20VlC8zT0=poPMwX2j|<Gc#tHH^cA zpTdoY?rRw9{^!mYr{f0%=oZ@Pf-Dh9nGKoV$Rc+NxaV!f0cwJQ8J-U;?4mBO66k)^ z$+LXpGv2Szu6-j1kF1Q1(tTm8bZeg8AW-7kSf2aZ)DseO4Ii>@aqS>=d$+`Y#1t96 zN()rn(NsRNG_J<&z^!`;yZB9a`e<l9kDush&7nOFvX%kX^U41U>H1G4`ENZ(0+#7S zSPKz=c3#c-9~1iq0q$73e>&7Ovqoo6Xl|Akj&+j+RejxI_9l_2d@M!BW^WLCXIM(l zvWsBNmoprwDup3u3t@_dV#9P6q?A3&aGlQ8PX|K<!KOX-&1PaEs0CwJ`wjCu+&2^# zthloF>XYSKOZZt<wv?C)+A`}ISqH&%nRt%sV9SB60#!qCpK<NP5w_g-(|0QwXJYk! zq-1ExM@|SxTr)l2DpFg!(p@Np{$3a+!0y1ml4b07L!vr#ySo4gM6c^@v*q;YNjPM= zkD_4w3UN7$#^`e_+QM=wJ|A}*A6w7IkTZ=`bWpt0vPwGQW|DywSH&U%;=tfF{{lZ> zk=e4BeXC}RaaHOymSs9RKy(ss!E(pPFHlyTGuq)?x);A1yVyFi`XmMtQs-$_Nfrkt z=3(=3w}LJR#(8H&J~Uh2#A(h-lYb4-fRU;zqetEMtXfxxefO9OykgRmbdbCq+mRNA z9w8rQm*xA;Ufd+stY_^D1FFG_Z*bW-L!>h?5g8TZs;WHbO<+?Hpx&v?_Ss;G0H*F% zv)+7Oh54!Ug*De5?8|itZ6W)-XQAm9{!c<qh0kvbx^~vIj(T{jmUvjdyO0wkolLy+ zp@aBf>PO@U$P18QivDFfx^X(95R~r68_Y?_u`&))bs)#_`%AFr++FzmrDs{#&sD%J zEJNu2?>rBwJTLf9UWk-~MLIlZ;_pZbb1{HCJ{D~tAW`)UB6%v-<KL!#yZAxc>Ev55 z#vQaK0RZ@Spug(_3K!&VQ=jUyHnU`9l2IL~s*?;3arm|%?0ix`eV?bp*OFH(2WnBr zTp~9K&F3+%FWI-Osq1fF9YT*rStroylG@UB!A+nZ`1FlZo4+rGrQN#IT^+Uy1L(hI zFn&5A#i_<#<_~hVc9pu$HHwC)K`*7DWq|{2(I(isL1JCBTCTKpY-UVU-4j)=QxDS9 zT0c1aWmTB*BWL=h^}n-I5XpF?P`(~4_58f43_0>gzx_;Nx~}&_qB=IGY*MtJYHIAb z&dy%rQ6bQ2DjaIMQqba_zSp}Zoy`VES?GaAq8H)%e5zac9pcG9D%T^*%bTpa=f`M= zPfzU{%pOXsbD=e>{dYa+8e+KKiPgMul6hv(ZhIM{C$RB$V=DWg+g`WsHIHhX`zDL= zsgK<i%|C+5*SiEGE+M5`nIe5mJmc{?w`kYtcMk$%<G<|K!VcCH%f<jSn%Q>p6P7SR zQF5%^GLuW^?oC%}Xy6bRr1=`NL!0nLw0my#Wx|z{f%J-p^Q|P`BdL4=DFe~NT2&~o zczzLa?PJ(~LbfqtT^MEG>bE%dxp~2$c0^_=I-tUcp)I8fe?alrsP-`NDQ(@*>YJLB zxjW8cBnf-JB}S#RsuxNhSyY*2vz^LRs5V!exN=xGc9)<r+o)K+t`L5}ls&CWt{4E@ zP<$V8UoCwngFDXp#CL`8AHxcMUXv9{y&t%4b8!*7hfAQWMC4~4W44Ikq1Mps?_dg_ z!Jq}{CuDb(3pIjqbTbr;U34`p(>RK#*{Ph(iJXcX^pG_mZ~<o|n?%=wwfEk>2#Dj* z8_PNklS8d^P!@s4mJv{PVLfZkTd}@a@P?z`XE&?i>tG0D)Wsf=fo{zLB`I}=JwDNG zv#MZLtUwR%wqw_uNDDdMai~f|((^U*!XD+$b)n;Z;#BrsV>s}iiYAS&SskRfEV;b+ zcU)Q#<()T}9dnV$e^2-h?!3rd0tTra7o}<rc${^;j+gIk=yYIgVWO;h7?*TH;h|6G zwjQNms?YJO_cCGExH`KccWU<>i9#x$#1zSrk;2Q0qGd--s4-%-o3yv->0H&9lf9qj za_u%lDsq<TdSW+56dT#z^e<OjUm@}cer|L2(2LR$pGliZQyk7{P?fQrwdPJIe<-7~ zx2lJTo0TbUR#mopSFMcr2F?#!UAmf_XMCFR^c)`4Qx8G~I>!UHvKL6tqK)Fr_&Y_; zk-`d{T@g4^^|ST9jt?xQmb!~j1KZlhj|5CGcoJmcgZYPoEYS_|SWG!rbG$|l(kCir zaJ>M!CODX1vfSxD=ZRIz=3KiO+g0zH-IOVn(eHvYhClrY@mvF6Viq9Z_cv#6=+p6s zxLE5d@Yqi>Pq#fH^bW72RzrUr!<&32XjIL~rLqyX_%8%*H9055zYBo9-dhk9kS_E< zfSWKJr87V>rW1ElNt8)joQ}d*#NLh$-E1#*{zz)~FS6BfrxdZf<NxLMKQsB3L9E9A z#NENVb@wgidW;tRQ{Pg<BwIAbIV(TgB!MglKjT9eP34qobzDa!k%QEU0hPhkzH65a z7w#05>YBD{POHaXQJ!85ahn#cSgU((=B?wSD93*%kFNiHC1JG$r|Xq7Sv^Q?*l-z> z@GYiQd@ESr$RlUJ)6IC<p#MlwVXYMJ-0z}@9fg%wnEPptVyqkXu(Xd~)t_f1Z+HkW zJVQiy@~)T8col+a-#~TDz_C#J-QXdjjRTFN!RVNs^)=iiu0Oz+6l0%3%QN7lUSgev zrS=R#<9z}Y_{xQ$y)`l65i!=7w)`7JnpCSw2p`-Dz*1~Vi2%3bnVMRgVzS-QRWJGc zx8mY-bzYZ|;{eUuw6%a%JMnF${f#J@k%n7bu+gdA_#l4o_yzh^3SSagQFEIvbqMv- zCD6Q`X(juNz6!%YZmGN#K=g<WD@^iWR(%1$x$glFAPVdx$HD6uz?TQ}X~C=k2n75D z($N=AU%5BX{qA_?3W#Nkr+5(&7QKm>^SQRHD(SYpUPfj)^RYaVRBIE+$FI3*74qLI z{_6Y)Oful0^51dgriq=yvze`jfd2_OR}XfRlZg<3R)v5L!3D_qJ0=N9*uSKezoi7n zG-44*coX^}^U_o9gde;s(b=q|8M+Ila#wM5g{62;ZE0uUd)!=Dy3)6wTsOWmHgb?O zJ#T&9Zjw8S7ZnMInu$yl*6*hE?CCD0)jI%>&{Yx-YKC~KH#P9;rIeDZrS=Ke&2m~k z5zDb!GbUO}xb6vaMt9ieW}bOF*(<V~OIyy15IGfS;hI-t7%bJsbC$0FMshQ(NETR` zRBWxEGJThLKatI8LvifZ%Yne<QuM#}jYV*hY4Pl}c3rK(Qv9nvo2~n(r`@T6Vwcip z0WR)tn4ZP?YnoPFyAlz04`~={zx^|VAHQvuMvKjiCD@<qToFXg^sA}qk&19>{f_Zd z<3LcTDZ-B~Ha22Vyis>+c$qP_(^Bg*RnbY&Bd#SlyqLrYH1`4pE0vNGJ50CqF>HR? zyQ_30<|9ea&d(|=y(DqBTuVWOxFG1YwrSYcu`tMeL8ipMfM5n~3QUv7kyQnPt=we% zGzSBEf+c85HQ-?8%Js;1v>vOpZ59+x{0Tw0QQCKcCMlo?V}SDwO&#w~{6C$&cU)6h z)HWI$Hp(bU6>vm~^dg}K7!?p{(pw<XO9;JJ3&Kz%(g{UDL29ITP<m&mF$AQ9-dpJ4 z-Erpq?)%+8@9_toa86EHXYIB3S^IgOMSl%8dIu5ZvYfc4fp~YgM@k!AZ#5?EM(J+! zrWx~$7u0?Z)GcjT^3yXI&zQDJ4yXWhj%445IHR_E!c_H%9^;-4xmGXK^xdacVqLV< z`Exo?D0=VYFK6i5lw*UKYAxv210wvJ*SmSs+&bPw4n-!`pgGHIMqtMNZK7<QJ4z}5 zzl2_1CRyM;NEXB8C3wXeT#?2T|A*K-bN1G~pFfk5GcGrH?zM-Nta-d3w)(dgR(SWd zuPLXsGowv(!keRJozf~XDn*B}6Z@-49%fVBIda0v`7+2tXIImT1NNC^|IQJ({(9wZ zK!9`@GYS(>euiaL*8~65-C~#H0FJMb4&T+A?PoQlcaNRg%IKfw;oduNHXY{3M9wO? z7BG_OUR;}47$?AIwsuz2T!Ma#pUyHJaA0R-*JRep)+=|`{Qatou#+^1)rRH#s=dnC z^%28|-4nBe>A5Ou+UVS@>}l654^iUGf@G@UkiWOLZ1nM4J;#CM2>VgGA+gPe<r*{A z>xxwkwR-+$ISTWMVd#i&_G(2kebgqUF++K?szq!X$aTl*v6~@umhrstw5M}Hx2(En zWP9(N*3|OC(6L_E_4;e1O(NKO3zu&^JK^C+re@Aj1q!D6imhN|kT(zb2XbLULbDl| z+E_#_!Cr%u!?k_i3g$Vv6e2u;B-;7cZHP$cKajwoyx#?lfA<I+u1VfrTRtwF5Jqqf zVL#hp{cU=2*jhn>1y9|fhK0WF`+!WyIKN6jssoXdk7_OkEsA$_nX-G6TrCVe1#}H{ z#F)RNI^?M#KV_2Ol%&;^F#zvtJDM3uN?-E4JkRA7bOtCdN18CU<7lNexmb|c${pA| z!(wVTYKwNHa;glOIiA^9GJIM5oL$#&VRm*0>e!C`zPoi0H<F?1RgR1fZ5KJpyc6Xt zY&liJl-yFB6yVQFT32hNE|4*vW_n;E){3JW#F>(VjT$-Fw6X8{0P)NGF_bVtE;r+6 zeP`7);ccQj7ut2OBacX*U9*FLObqWk6pRkx<uaF-YI1GMEh^;n+ur)qK&|#$ZWcUA za#5I6D6Wa%(EaEC(&$v@c)LUwso@~ac{w>?DD3ui8&kMGm%DpnJNKe*N=@JL{>)D& zQJbejj;kZP^cYP483KK*4dzgOPM!)4{emcKkW^;9F8i$4?mcvI&C9M~7c+mZk)O+1 zpM7f6+P$nliBN5MBHJgZo{jO%w6@x0QA$hv7{WpuJQ81d{T?ZDX3Xv=MWk?^OQ;PA z#OT^9eIh2jg`cNwbr%Mrwnh;ZWkJEayCl@`YJYz3g5#2r69KIeyVBf8)v2_wRxasl zVfp^V_L#kha4K|Ylc|5IHPW+VQB{Oihr$@JyJhj{(}j=uHpT?lz#n+^s3Pfkn<J?< zV$J$-Vfv19SLvTQ1r}U87Q5yR_9vH<YXMyeqi&jz4inqc6PkW&#Kwo;eFFWH>IMj( zp*&M&A|3bMK7R>lfL}nj!Sq)C(v7bd$%Knr03HO0gFxWz1L$n-<IOLNxr_JUZ&`@D z<*|Rv_=p{zE@k`2%wDu`{N@`~oDlwI>s$GY=OeSA5jSJ0HOiiLkGB6aE#v^XsPk8^ zW=R3xtcW=%;PCrs=Yzo{Ie6`X8o0<>14(J1APppj{-^J{cIo^F_6K}$50Nm75b6ew zt{1wINj40Ns!6W*EqFvI?||-Q`VDs2D)w4Fi89GL(?Z0oqlzJ~;jS6?GN>H=VueKi zAXxV(L{$WQQ3>(IN{ARQo5fvXhOQ#f4<}jXl4ysktjWL40&rifoQWmnIqcjCqSR{d zyA~%aRtnGh7;jfsb_#rMpOJ0g$b^pC5NR;+hH0LEAE->mOjPA0A661DAqrK;ZTzk^ zDM$~?CSJq8xQ26bve2|mD|e9Uf-J=>8^}-Fyo{grRQF{@bNQ(~O)f&&#Xg4I=DKh2 z>ic>@O~;51OM=?c6$K>c<VSWZb88n$eu>C#{7Ly*YCYPf#4tZgRK&iU=k!Xe9YZ4` zLaGnbmZo!53v@})oY2Xo_58Jc**U$5V(U4B!KFOS*MXubOW_D+*=W-rxT4XFH}f;g znAPz1)@dDu#ps(>W7CyPB*nVYre`1HnlCE>$r>}*Hxk2dbC`89#c=9V4%2^V=M8pN z=nRovwQTwVj2jbeDDn>_2aC%8wyUTnr~b8>5PfP%y@;DhE;Rv)rk|SGTw#igZJDY? zjJx+No9Pi>KojkXUlbuRn62x7X!z8W7x09{V8c}Zq-(m9!Iq4JNDtuwl0^Vfp(cM0 zye+@H2Gpn_K;P=xZ3q;+9s@xJi2R51ujO4IC5v+BGnQK4;^+Z7<rqn=r}z*?$iw8z zx0W=%wb}RajiymC%@@YlT#rz!whn%t2V53%`=XnUjUJsVqcHdY2&~{YH^2i^OG`4~ z%~}qVM6W==?Ab*iLVxbuvzNDCeIbK|&k1o;U3)P_3po#zuU-l{KL}BgKX>cOg_rbC zF32-cCOv%2NgXHhVo9OP%>CSlJ9<yLZW<-(ITU<N@^!M@-xJV-8GEkS=SY=(G}-Ze zG+ZSh7}ubkzcis<`FG(0-RXF{T=rNg^pkaCY27ArD0fXJv}xRry?p;_t>CvpV*yXy zLf4?;k6UX~c8w0BB1h$J6+<QbKMu9Gb1LqWB1`VpU+WFj@h=@HJ8SGEDd|rf1#!9` z8jh9MJ4EgK=2)nlPFy!dXUdB1sCb};Hqo)`YiYH`YyQqcd71w8Gts4|Ir*|{aFp-a zSc@!wT@(h)&^S2y6-`uF$V9G8D<ETIXC^awF+t}NCWdj0Wz*4xP`pHe)hvuSvA<8D zip-U&7&9%`7qoYs!^w3-YkcvzIw=v|_4vz_GrOv_pOave>O-YiJ6}zzq#}yy1Q*IO zPMsx2RTl1QAzRhlkC)EdhBMt|0-2VJa7xQ2+Yr@@kSrdqr?j7{h1_4cKe@4Z`yr!H z8n@;5OhzH+O-9HEfEK*^h4%Sfav(bY21wIDK#Jx!T#@WDK>AD+WP&uWiu@>wqAJgV z<9ittnC~&o>vRCJSmc!%P#|1@nOn7K*oGddp3AO-ql&R%WF$|ULe5Jri{@Yb&pjX@ z+kZVv;D)ckUmwoFJza%Lnj>09dmtCcNb=_)fHVeleqR6?yqC8qKA6Y@S~Tz+c?cvk z`v!2?a~==?1(LV_W4KzVO+~BaQ<cMOYZpFircteW3Kz0gS%nwE6=kPH4-H(F$t~S~ zcXAV_@%M3=2UX1(!BZAC&SEURyvlHfa?<@&BJ`z}lsR|#)YA9X3WmL*Sn%~JJ$zra z*;CfJA%3LX{=~gDbXZ);1XMpb0)(slv)Eq{?9wkt1-(z>JKVZwRqI)O;YrMsN|TIh z01bkDf@Tg<>gwGQuMsgPs^n7(wtXR~?B$GXvvK|`t(o|dzl<Avh--v;)TO456VeV7 z@iEAg<NLE`{64JKy`y#Hp{5i`FDAT=t|kg-jfJ$_H52Lkvn%qpA`rrYy!h)S1oGfZ z=NrvfAisU?+22B7!Ul5gi_Y+sTXzA>^f{;&ZGVvrI0cw|?sH;fQvu*V=wu)i;BQxM z0pw^&3nzSpxY9!~62d^);rg#icyJ;;T?`-P-+v&#Eio`Wd$98aNW+mk9b`&{Ldsxa zYsx(Hy&hHlyWrVG+gCG=yiLtk1&;)1;61<nC+OZ<y1>j&I6$LX8~Rlsp7|zK-Lx;} zgR44MJC8*I^!BuWufu={(71aEf!vfA`2CC_WU@Qt61Rt~5R1jt*n$^bq)S~duf7Dp zD==@AtS}5NhW<IzOY%+J0X65TCFOy@^Bt{%E)F502k2ciJeFBOtP9O`?$rrf7MhK+ z7Ud1S7Y>+-qEiY|FFnqAoXZu-{vdW2avmsL16igfmZvXF<iRC4P{O;o48F&HNrHbl z&tnfc2hxb=p_*b0w3;u6!FICkKoJbG?F`8CU+2NC@|=sKE2R6AquNAMS#+_`2xapC z10hQ=|F;7LZQ(<ps?3Mm=iN#<<xkJ9&aDDMohc29&P{z!EL-KlF|NH+e8%y&YBl=> z?hr{=#Zrm<tBLnsr9Ng%FyUyjx^q>i+NFy^lrj@eFF#`IP*9$#CQ6lTKBE8{*askC z^YYru51;|?T!8!~Z+}B7;#EWn-y^HY0-#2OHfXYXpY*NwX_B73q68eP#USEwZ>n=R zELiH@HxK&rq<b%<-hGeFrDW8-8!`HPg@O7ji@el3>aUh7V9!_zC7_^X#m=on`(8v5 zW@{#rXZyl3#mulZld>k}qTAzZtz&Rb?VrAFm-HwphX;fTYGR`4xyUE^^9`2~gGibM zg?jND_84cYh-ByF7xVDD*&(;zCr48RSX}dnyfRC1GuhdyZ640f<_%}(?uorNt0in% z|M=W+*vsX=T*(Ik$BG7Uyy`>>1L^C*U4Bhd9R&;`Ku4)ZKwtaz`M2j{em@a11`zU+ zY;p(?TZj)Fh}SaS1S-tZ88Rh2e}evloB;p>ET(hr=c_gaD64$gh(gUW#S?pxYgtm> zhF=#e?pScNiAKyx$T2h?vE6j_#tr#=CRQG-y(PNZ)x`K}*sL9?JUyJ0|DR{~n3D&1 zKo$p`P%Izu!f)L5?+3^C!s?GUtCX;GhPn5D-TLxMvn2^$g$li3@-fY-kmn%9y0BP) z>%!F#O(3S9WEJ<%>))cRkaxe+(qcT^H#%Otn6>(&osT6Opt!#S0~;UHbyjWobkkBx zmA4VBR`uCblSwY#i4#t02yxzcH8GOmIR9!<58;;m#C}Lg%Hcp{7L{CPT*fpk!o3gL z(3^OauI7v8vT>{l;5Q+bc-rqp?3-ZR3~v$Ji@IisF#V6uq-@V_+0O3vY8H7S8_b!# zPgN{eqx>F(C8}x5$iND)2H)fN-%x1y0-32gQY53U#cm`c!uyn+Lc?c;fyP@Xv_3HA zm2N}v%l(GpH@q8cpI&rs{P!Ji(2mb-Fnr7VM*aO&WC7nJX@rxf<4CoG+&mt?XZ}bu z)gegu5s#pn@q1VUn;NcEf3`du%a%Cogmoeo2B_84HLQ&vRH6J<o$C2@Jc^K;N{UWc z<)NTbYn$02m_ADO)EkY-vvR1$I3DZaGFUixkr6Kb9M|tB>L3bPbq^|V6%T?UVN$Nh zQk|cTR|8Cs@q4Ej_bg@#>f2rMk{-&P)gEG4+QsEMBRl+!r>o6Ms<V?QPSj+giC((? z=OxSiEDGaVSsbR$hL2A+!+&OBXY$#g%t3|{a|CL17gwJZ(^-^vR5(7;pE=G)*j%$C zpvB?iK1f!Y$1FA32`T;lA|oYa&_gYo^<D$rN>qH+y8g4mM%v&S)@U`QJhgh(F!q6^ z@oT8C`i0y1{R=yVdFFAEWBr(MjmoS<?H>iHTd8v1IU{)~K?QZ1+dI5DBWw}XBp3eL zA-TkL|BfI2+B8_ck>K6j4GPMfItG#S%@?p~XP&P~G~R6#BFWCuN_Kk<gp5in$K&$B z{0od#OP90jc@3evPU^Cki-!)F_Ov`5QuSCh<kEai#a1H65NuT(#Y(WC_v+fs&PF7X zeNH}Z%qW9Rq^3S-?AZC}Ac3%SwlR^|r^m+1rf0~@_9+RDtz)=s!Jr4)G`gO^g2`ye zMLal7JWr!+scb)s6T8%$7y*-|QL6xGxwV|u1vlR)_q_lh>OEd-(_Q1RPw%8QUZs58 zv4m}9p`Vsrn%bOkk(TzKyyaHyM!2=Q?ccY!x{Ato3R!rQai`!xpV+5e*HNSLPg&o$ zWlg=*it>DyoOE4}yiF7dPK)&cP!A8YS+nK203s(nS3QGB!&W3CZ0dXLPulBoo{@}Z zyp&&)17rlo$9Fva(|#7c4UHJtxbt}j^IC0*1!jZKf8)237K2tpaQ?J<P$JeBpl9c> zJCayCZ9ahoazOglscfpBvj$QQj%EnFhU>|wS%;LB@~nnGNyjVp-|_n+p>Hl<yDfy# za#ZVF$sftk&?XM)O{)2QJ@j?DNAU11oJ?$r3QZH*b)Aola-+;N<Wzhn0^n*OO8wh2 zL+*m`@&R&*QGe~;2uCB0<&oJ=7Z8l$mItv6jLhlfMKt;3JC(HgguBHM7x*~gfg5we zO>-;t?Sv%hb)XnvLf5R1ewR>Eq%jw9vUqSJC5CtN)|u5Fa{L9kdJx1aM3jINwl<cr zUXlJm=&Rms8)Y^q#-XO(P=&8VMgiee#Dnh#t-m1u9Hh!|MFohbm`Vpl|L~G=R1?<{ zR9-!8RJLQ2UOX?EKWjtD*V7sPft%|3l|ErUfSSAi!UhDvZl<0YhMwwMs$ZM=%0!w( z7|u_Dx-}B3Pn-0L=x-sD9Kt}q2EZS1t%4@NQ;F6TO0v1xn#Bm-L4Y*de|!}x?~5*- z$xSNPR%-7pYCd5}{8vp%JX3K@3nhGGl3%}$Ec?LH2+B}ZtV-n>&+qF|n{*reQ)8NP z1W&<D!>X1Zeo~sl-!40lv0JXJ>nDC1E1o$p85#2-T+A!cYv;PmeoXnr)k)1g5yqN| zj!phn5vlHGxE0_p>)|yo9HI24YSmL<V*BUB=%)lnbGOLJC3TbAXI(SC<38Y|&6xHH zrx{lLdvbCD59ClmKmcdHq@vp)K0&#!v+_Zoh8|&_AX%&0GjDS1$rJNC@d%VlDiRay zU;@2ArezwEDDtGohV_1nhTr1e+4riAYAiF={SR?PHGwVnP7t$711-~&0mX><!$WwD zEjDkmb1dj!#WoE~U3ix3sPZQ3sjO^tyg=6QFGvnfbSWxk<-k*>{}>g+nv0V~qmelq z81?Cr1r-`73zTEzY7zOdDjd=9#<W#nd>u#<C*f<@ypq+@?S5BGQv&pfRaA^tE&ILJ zA*O_|rG7&<7~>-DC_of1oSgBdKf3rS$v3Nl>joQLZ-XIm@-3=Xea+NUO_`*Bn>CT6 zJ;$JPw=tLiV=6UF^CGE=DH$Y{DJ(68Iit?tUxsw{d%FrN2#qCej@14E0&4sLPn-3h zu7(k80dv{9uaF>IRO*eU?^s_S=~^2P&*IfRz+bI#RQbHZ>Vh0<Ig}i~O6>8RFFC4` zS)8QBhlLJgb@z4)IuS~RiuCvvNfV<r7OL(zJ(4eGWl+#wZzT1o7L*>3@G#QbE6iFu z!+xaU+uY_Zk&qtokt`R?fa}0-)zf6FKRsg4(i@OXi-Csr|AM&K_m3!tcWXFpZ~qv? zPP#3ReH8!fq1@Vg5a^~UX1_!mxmKoG>!ce=)U{q?U0*$ko{W<Gqu%Pp&m%L(G=3E| zEjwscks6cagp|9vd4O)hI{ovg!!WekN3_FUpfyP$xnhEnzoJlQ+hJ>IVXz!tFBKa$ zR9M3~B%7iX*1ddtL^cVnyYPK?ZIu#UXYBYx+;^YZ&ZsXm(`h&rF*SUI8bsRnEH`3V zY0{-W=i5e=?OJV0Bdrp4>e?ORv8;}wuCdXDQn=eW7p7)h<e>de`lKhl1mh3NkG2wj zj@4E7b(G%o!2`wo2reDe`@^BdSQBOFdvsGkjW4WF6q=!C9)y{h7+Hb)T8!9*w&11R zoPUb*I)5F9+s5i!s{Dx5Nzn*}`QMZkkh^w_6~l%`tj@YU$csUGA4tmq3arCKkTod6 zgIya>L)7lov%pN00P;a2f9)h@`A}x#7?t5g%+Tl^5obZm(fB9yvWSQbXI=3tbdOEZ zf}g5xS8VaCHAYK2?2=sb1A<~}iU2qDLq(4S&UC9mHw<Gx`cNrH0b8UdIso#dpIw-t z3Mg3`xwFB&HJoy}MeCL{rb2X&8M$8)bx;{(22x?|igk=zZW43@!s%oE?hQZLq-tke z@U|PF&2a~8bZi#?K&yj}Ft8(_r?&oN5fE6<uKNSN*DDZGCJ>3Mi<pa|g+IS$z5KLY zzkBD7Wa0gCCk-mebU8YHY2KxcZrp6Ig-Jcak!7O?^B`cPSW%9k_<TP@VS+U+h;fx< zhwjG`e?cmv1~Y@GK)j#`#b_^0ZZ#$#Ofux)`xd9ztjuHW`SuH6T}&@Af`qRki_afX z@vU}P)r9=WaeqpW-Mk}nT%A5&g@=l7cG)Ka#m3xGzJ00Nm^g!@1c{6H-g{g&uc1U$ zMSA+H#?Kb;Nt1e)t2uerTWayUs+Dy`V=kso#H}!w-w{y=vuWH=^MV_0i%m(1h?8y9 zcXF~&(w}tjg~8I&uo;uE>bFsjj!SwAx`%YLNSXUY9CbAi-wJg4X1gpSHHz{sQ|+zA z90duCh7TfzZ?LQb0B!x-s4<@dv31=Ebt6nECk!U$@tre0s_1fV>&$2k|I;#6<M!zv zJCZW_#sMfWud{hjx8k!#dRuF;M)1_}lAKkPUDsJ8rD9BE?$lqF>zMHsy!Gj5XVY&? z(lk%4kTufZB%0M^YDO1v*hkW%Q)}qu&6&d(on2UujxlNpRA~jzKis#lQ1oC4`nP}J z!Wvbxd87zz*7YmD-DA!MCh<;XmxZZ*bn;MPPNi&Kf5h^P$F=nyiy^1iYiT*L)t(cC z2C--_U8m}h`xg0_QQkK=Qd@e4^uxZ6^l{?#+6<<B#p>uDuL|*oj!Nqv>4DakvOj8q zW{EiuU~F_Gly3|G+H>h)bqh)UOb4gWpliFMjT)_`g8ciuf4;Kj*eWT=J6%_(5J(zK z)i&BX99y=5G40$=>9@)P0AAo41!|cC1jE%+W6^CKz*5_kuaoAj1~YV3KDXxvIu^&e z;6_pDQHBLag3D!MO8R9+F!$Y2II~ycdmWthVU=#hv&q#h0=h2fdaMdUOKf?GC7EEG z?avG)nc|~7FvQj!ztVD7>A*2WgCRpT({aB0l5Y`A1aWSA=g5C+UfRtt0M6>}6t>#Z zjLjL5Xsy4MVss?tYwv%91zTfNydv%8I5mW?u7!I^t+v<q_KUgiKF(M7cc0Grv{JUk zd&f_UpO$uJPTaLnCj2K-Sa@ou=jV@VC(;@-a6CrBC5Z`R6W$DcZFObtMQ>&a(L)Op zE?i<QHkmEl(-g+wDR?F|Ry=pdVeKf-Z9XBcD7ZY3>De=-iC+-SqWT2<4I3~DLHc>( z;JtwYwd3{W)t+ZhcsuRK_;KTwk4=;9rbx4=N@+b)lTT*+0C#48OR{W|G5TQBX$UWQ zD9uVS7b+f5J@ouoHiesWwkEZFwnlAWa1EtfZzJkNk?+BSv&wbsDDcX0+YA5UytSHc zDcDsK03BkXe>S9pW6e3Z+PgIQx>y!)_i>^9Cwob|GsQD2WjNlAqNHQJE+GVEdT7S8 zd2?C8N24u(6&^Yd^~3MMsX{N+<d?A3xlB!XES60lY0bL_=-bSUnCPzlf?RQ%5Jb%3 zQ57KQ&?j&CCxS_z4-3wVBD02!*Ag{!MyPUY4t7ZbSC_g?IeLh!SHl-%1#8PZ3y(9L z<{it7CaOvoqxpch4#?6VM#1#gReT;UYz<8lL7eQ1@<S>|cYB{OV=B!j%2sK|-Abk@ zJ9f+8vPeQ>4|}(eh>NI+!39063E5-IND_p~0fVDJ0h5fc{IyQeb9L6u$d(kXP}4L= z!EmSCPvyIei%X&pNvoBa1zseBh$Ar^&T68rv7)fSIJ!5DYKX3EeE8wYfZNdLvfW*( zQ%>{rdQZ~#oV*soKuJTG|7k?I=~ju&`olR6&zrHwf{H~(O1b`P{@y7P<22nx;JUGI zg1FKSD)eGm61M#7BOEKeCyFbMJ`Zkle9FnL7gnz1hbgm(Goz)QfO_pv=Ok0Ecm_qk z$0~G{IkPD({Yaz;s_&QvQ(n%XLU2Z<_OVSh=18t0r(J(RU@(Jrq<P=SJ}d`-$7ABx zPfz0P+Lmypx$Ze5*pE<D`sJJ_sN^<(rmQs429GCR-nD1lger;Cw6w~{Zh6Zhz7xqY zON)hF!fG&%94Z)Befx}FouBpyaQV%{JGI6!#=QXZ&O&p7VYc0%%3@=dsyiyENj_dq zHPZNLH+Td`GoQ)WPd<%MsI}PllLHvIh@Md-UaquaKh)aFiKSC!rG)o8fj6_NpR=v1 z6PXu|3KSh&aOQHvm-Xl+ZTQr&26))-wy0r~SN&ld-9KhfA`{TMgLQ@xf&7P>d-{@! znkX2JT>M}F)2Vx9-k<v}HQ0QcOww+5ZWEm5rs=R`ET~4)0dh^`G(X6;|ChP~87_Is z{JjXBO1D7P&fO96`^BxhkQee0i0!4PRgB!pW`G7mnF+@5?1piVuO<ZWJ_hd@CY%hG zd(E}ZPVwUb)o&W6>Kx(+9K^g2uOfM7&kP#-(CFqY1e^F<bp;r7V>2Uki%3)YwzrY> zEN|btz5QS_G1Bo^(Q_b8hVO^FF}3h}B4g`zDqAGtlqRfs`~T!!LfzVChdl1>ruP$e z6JmBMOC!s`bd=DnwrV$@HDmlO$`6s54BhEBpn6R6@PX)%ju274_W}%a32zmyynPP2 zMG1c|oGe6)D;EYFc}||FOWll5$ZYs<XR#iUFy~IDdFDUg2m=LAki0zilAL{EpVe#? zl`s?NoJ$bX_tN1qR~t}WeSPY91GkmlukcCzq(4|Du+%3?0?im8FpEDHm-FxL=wBDt z)6P&`TEtE4p8R~<-28ZL&Xzc7*;^5SD4m_znAJ8huK_Hy1X8tUVL+I5cT}yC?9oH- zBIVrWoIDkv^lIB3I3c%{14oPqkGCrpPHtOR4h_-%obP?pw;jPa)Jl9*X^z;3x_6&` zCa^Y8S~Rl_8mwd}Nx2a-R%oDMy)|MN2K=6BecN8~8h4pg*ZJ(!zhxq<iJn?^SoEe{ zoB`lw&+0RG-$VCLK&o}Z*vgaFOq|*LD9b!ZQrQKEP@Zb(@jR%<NA7vUXnV3trHw7d zSB)%tm2wXneT?E7nP6x6q@~XbU^Jw0`Evj>_9;;8B_#Glj@S6vSheTjAEk!l8iP&K z>~DYnbP87m<2A*UCmMh!^V+@MBMp}v{UhU~tS&t+VI$+>Zt#}~<Y-Rw+{0bTv$;A< z&3M4=9rv|kQC>lho<>f*u~U5skCyLZ7Uz4@;FvjAPow>^F*nJ?+;*JN;JxC%TcrDw zwc=_Km2+GLsrLwhg5xcFQrJ@L!KU{qk6eA}!IqmBtF@kZ0<%U{g|6Q(h?{Papc1!) za@ERamaOi49gn%+W_#2#Lb89mCxW>v1n1uMV2M7;I@fz~#y@~A2;AjKLs2ms{;)yN zYu;*ZpKfZKKfYmo31<{L&7iQDnfO{(-Lu=+qZBrHbYQe%qT6CoOgJvm3e-o5#4ab4 zxdbUH751!6YtL>U#QcKnA?A-T(bQeF^!DHS$Ah|s`HL#%v#@oE8paU^#=}R@6#z(K ziZ8Ffz3M-<2UM<Nk$Mfao(%}p0G^p{;m%1QaX(7&d74&qOq|u!uIZirj0jU%%u$xb z;+%0zO?vX^%v$$Xrya%PO}?nAL|%s?=h^WOsb#oPv(|D{Z!*?>E`z5c`i?>^k*#*t z=T*bPg43ZvLT@`Qb-S((*Nz8TQfvmlMN(J8Rv_h2enLk_HcVwBbm*Fi>VPwRjj>Y# zCOQz%8M}I|s|(>sL`iziL~|u^dTeX*<nhcLZrcr{KYY6VJ#c8~iHGEV-Fmuy>ztQb zG9#@gL0#taRkdYOfDiy}!J^Tug@@_*zkOIt1EgG7Vnhf(_X4~7C$_iUq@&vJb>A<* zW|@Z4=RBJ3lCnk3l3URjLE3eseX`^8y>c8rRM2JJq~cgWS{=G{=OoqeuK~^s<7bG= z!KQEL)&X#Ud29467whWcfdXOvkTX3geLur?S8~6GMEy^1J3wr%)`iolJ?z69J|0pQ zC@Uj=TRLFAm#SRWW-j_;{%~ymq^dxj(`D>X&nF`y>S|EDL%D^az%|z;72<OhM3eu} zDAM5Q!41JoDTP8glX(+$<`BoZac_Keh2MCDtDS2iM`nrC{koen9TO;w`Dkj-n4DLR z6cVd5TKxBJ|LSU>W586~nPGsZFN@Ozn(O;OeeRC)w2zO|kB?Pe6hx|0CQXw+Fj}P= zmv&NALSJ(>B<>zn5C$kKJS0fI9{M<ekumT3%BWJOTMo&i*5CEH-mMyOaKYa%9Tvmg zVX`a_aVA8QdXtO;pStd~iZ1o8)<Cx2VVS&RJfOdZR&KNE1D#(jl2l{qj=$rI$}fnv zW2)TQNTb2QVFRde2?SO{0mT37I!j99sYhc_+b_r(`zRQe974g{kWF$Qz`7z212-^p zvK)H*e5)xY1_#@zw7Z>OmK!+zC+K3NOE>d_4C>la{jD}@OGQo^h~ud0N)G>(@W2~y zz`QtkVRY<lSW!Je*igr)>Hd8nonbd&O`IFu2z@`E->RpWtP&mem*K63tE`pRg1Byy zDno)==iJx_x;|&MvrYY2RXui=sT&uyS0*SV;x7r9Ji9TZZKH;NIAb(0$!Iq0p>U{M zu&>UF!XzhR0+J;?yH-|5KnbCGM2B0gr_qk6T~7^_@5<A^3(Q&3lXxE;Pt#sqXjhe` z;um;Ua~@BcdtjmFKOV3IENcwyBpN^EzUGmxFmF*SS37_BXtr|v$I6bWK%wtQ%+TCJ zcRANdFm&r)TyZ(A`2_)a2I|6bNgqkSQB*bb8R7P*f~3o!aICW6qo>=4UYOOgo`nk2 zM7kIgkH67Zywp$PKI7{fAHa+kM;fQnE45R%z!PAl;mU6R1sMQQ%;$J`lXa(I6}%mc z_%#kz9_9Q6p2z0o{AM-V*_eOQp|ZkrzXmV$iXr%)mxx)*u_prD^*7m9DkE9!hh)P~ zOfHLZt`^jmIK`&n)U{J5p#&L~e<g|Vpe_9!z39tnQaXN;6QZcSQp?yrffop>E&Ui& zvkN2i&rZ!xY@G>G$DjC*>(>N*uU6bjEcY=9i~sw`m4nUW>KWSPy}HV1R7}HIm2NqR zv3oewZ9;ESrPo<HQkt)#)7T+(6*KA<>!@}_ki?J^UmVCA453hhzwLOjU}CB6NFU;& zHAZyENo_^$Y6x@w^5-n#&GiN+;*V|5@$$B0eW{~FfkK@|CYx%UoopWA`^>J2QuYV{ z@OWl;byqeFHndZA%(~Ze38W{o1nI#^vefj_n>I@%t9>_bL<UPks`LnERs8JxgxBRT zN<&3Dy^nOCSC`sNla4g(edI6^eikenOcFaeu|^B9UK#<noL6|`3Ws~2W&HR`g#9s5 znn-P>XZS_p&&jJ9_{KaWY2$OC+|1&S#5pHeYZ!kM-o#o@O|wMHcQbA@*Oxw*Rg7QS zv%Sbo)b{je>k3o3My7ChtsExVtA2t0x`}Y^M?<C)sT|Jop{eN5tkDh865i=Qc8>Vj zw|eWW4SZD6{Kb#I#yYXx3sZOATz+E&lZ9PYsB6PxgPTZ&E>f94h56IFyYn44*1GMD z_~6wOIxh}A9;s_0-ssqTt3%EirPC%M7qpz(3Qv%sT=r|%&42o#UFz-BhpzwmI1uLB zCl#cm(PYy+aNy}sx37s744TIk#Y}q9c(nZ_uEEegua|rFBWT$b4o77(iW(WwB3yyt zpf3`4FP-kOoeXf>BMK3B9&<93ubDJHJ{`Ty<0&{gl*x3WTuP1gF3{HT(xd;Rg%T&o z%_p?mV*$Z_14$}6ih%+NgcGmBuk%6254PPKejtv~$TNFv0h%jp9e4T<={M<JU|@dK z5q=!P;98+UmuDuSjqTM+sOi?wxbV-4F~4+x<ZAQO;@HYTy}?>?0CiJb<F3Fd-I+tc z#<NYM!E)#>n0G()1QE(QJ&WxZM6WS%eNop{Nm3x3`G!!!^PcbTE`l;|0MJ4n_1c51 zGFiX&9EI+&qOP)2GhS&(zn{kv%npJ<Z|;>>@5ACAiT=y{zW;k7uOrh2%*71~(6Y}q zNvJZ`e_2nxd%0^}cKrLKvnll1Vg*y3e{gxcYY-n|L8*Ou0H}C1<=jihcpYdoA8du< zo8~9cLAri*7Q_>8l{WE!ENR=mbUFtq{!oTam&lnzb(%K8v(T35LmA{!-QX7C#Iw2j zf6X|mar&>V+q*1qRxdgfavH5U$iDRk@-MqM?S-^xUg6Jl0RI&+7io?YWq55>eCgW+ zmFwKoK88FVFuM<lT2zju<Pg16^7jjT_%xVRKiB@y%-R|Eb;P>E8IW#Tz&&3B7I}Fa z;vX)N9XTCbd5rKe7C1(y-JcL1rT^SN(f?*>YCd3`T4S0(m%c&Y+DAM?X&Y>8dV+WJ zueR%Vme!A1rypYlRrrGmb}DVk?Rw=~YOFY}?6%`TH9d4L(WNGC5lbW(9wBIbS0@t9 z@kI&NbCgjt0ldttSS$azk!B1SX+55u-7H_i$>!VGyjdmgR5;>UQ;3MjYUvBL#hx7# z<&LSIJ=wdlMr}9W4?a`bwXBZD^cB^uG~yo&S3&U+LmTMjqv}AtnR%mas9IZNiAE$c zqhAyEU0nv(6d)}n<|H{{yW0>DU_Bg(DmY3x4z=7{Z!fnpt_JmezT-HebP&(<>~`E= zJ8GbFUgbNf4`so7*D%E5QhiNAt)&8Q_(FN}+}UwOGrj1G3G4jUe%yp1*C(AE)?|gs zZUlwd;Gs@9L9esLFj0qSiZUr%OQ|-SgLMlQopzzO%lS=m%Pm5)(7hT&XuCpEh4sK9 zGQE^FRl#O-aEHFi*^Al5v$vcywYG%yu20rb&%i8cswC&+ZLj!*Yp>;dsmvI~h&N8y z4D8(ND)_N?rO5^J)+tvZ8@E2)|2RmmOuf+iO#089&w4=uJ-;9<B~#le9@=%*(p(H& z(eGXD0)2N=j#x6G#&Q$^_N7uQ=!op~=QwHpq6ZU5jU$I`X#a?lr0-bP5}Ml!E3}%| z_v44f)i$k2_TkJS<i7B{pNZapln1$cqR-Z|@a!kHtVi%<BoM?jQ{hA@kJm`u9j+-; zRMr`bEaFhYBjYe}_23s|ddD9d$Fm+?d;cR_>5uYQWjFMv!y2(n@x5P=Y=n|QL0@`* zq%6Hlt4iRw6|1lh-9y~%HPPugjeXRDU}Xf`7&cBC?PjP`S2C6tIkOs>yZqX&e8+0a z?@q1K36Foe1|3lE5cvgx`iUJQJociDsZCW~yukdd%Y&u%tl<0QL%Hso??s6}Q`6YO zhayHo?<EqQ(#j(TC)RlUa~mRH^Gh`rnxU*Bsm#kRB#+%sc38!fJ}2++ihhHzNUd0? z1%6<xxbEBgZ<YCu!Kf&@-W?Ox#GEkng#lB<0H7N)?$mqEoNAIhtv3BCvjcjoyZE08 z-b0DCy2+fm|AGXbIn};p!((HMJ*f{XPTvStdKu<zl^mt%DKD{D*I3a_a?PM0f8Ner ztVl~6^=HwD3WB2m9}zz%YgF7Pop$}YDP5;*QN|nmiM93se?|k0&!PawW>v3|QMrJs zFZ>x`P+3=BA*?guZWL>;X`&HIgAv1CP%JDXy2>V#v}=s_mK(XyB&F<Azz*%)t85g^ z|5>_F<B<GggYCg__n(|&TK?m3k7;6-Y@d>9)F^4W%)B3s9?sDXU`9#;{qVHh(p8Mi z>E1MD+Po{eHalm>f+f(4S}-v}RzUwc5d5m>>wmLGbb~tL@o?lpO{4Ox)w0<lvu+%g zx76M(ytwvZNPa#6{pz2^vY>BeK4%-318mMbY`XV+_W$ai+b}DwP~ab;YPf2tLkLo1 zcJId_Bu>XFl?I$1oTiob^n}Zi6fZ45>sl_lnW3UJ&CHAp{jw)jPuv;xo4F_F6+4(K z7cxLhKFvVZ9-IGMOJF6IFAQ#dM5shX?H1)=wT4XC-V6i2m-3vilZql6{q^`e4uhoB zxkQJxW7h6U$662OVAuVt&leIO4i?HLbGNa@)v5WG>+MGP7Df-nmpT%(b`WZAKb9Kc z^D|TXFgh!PyrQlJt<)-oXUcZ@o3q}hyePXGm`OT~ah^S9Db<r$zUwzxD%e&j?H{76 zM>O$5)EJSleuCe29VUmXXbmgITUz?p2ZpK#Y>Nui&E>2oZSgwN7yOE}BW(ozhUf;J z_Eo+SzLf>F;#q@A5m4aayom07kMw_jCUiDvCYw8kq#0gyTTFyDP}^X>%>}X)+ktYY zch=?RaAMKFAeWJo(&y{<ozvG!Y%IFj_>a3SIL{n1$~guz<Q~IJmDHkeCn;+TrQJjl z;}AlztIxjOuZtjUQMN$%;nmLT(yg(k_jt$mG#4f|6xtik#bqTiCU@Ir#Lo-MnmFrp zQh`?E9rtXFYwuPY%aO?E;N5*=CtkqbNgHYlbS#2WwpZkK9&G!pVCNfOE^P0A-4*x+ zDZxl<SOjx1K7+AnszDpwh#&v)P<gV5Ll5nFn~0Pi?gg>(%kgmOO0UBUQeUL=DtCS= z-;dc%@dXi!AO|}4ZYQIHzN+@4#?9s00__P=qnWflTilRMd`CvnPgl}Ir2z!Dm->E5 z?p{dq6?Ks~9jVL|RNw;03#4V8$<_sCly)qQWE?Wf2GwRtz-TTAdQp=~@H+m*6TnAH zv`U{s%LDsgZEO{?ZDLuSuE!Yuf?$-l{euLLL!uHrQ(Y(3=`3|U`%{8rM@0g$6#f%} ziA<j6lPEByKchYoq%omC+P7WfV>r}F5ZxLjxTl3{p{f)$Oz1Tz>hEZj+9;8XM^uzD z5u6R!C2Te+11_{-LO_21;ZkdCUyfj^2^?`S&7W^56oX5B7T|0C;*?#lnHihvRkCy> z7*JZX6}c~2d7URv*Gwme-aJk$Bp<QHF=aA}KDdo$JIzkYI2_F&RuAY<`w9AR+=y8; zrgwHo`(E<+#pHZm$7{{(L)rAA=s(_Lrn2k(`w}C%M><9hD*CvZg3$D^t{K!o<@WL# zns_AUxE;#l>0!R^p~149I&)NZ7Gam9@wg%o)5eG^#Ifnx2!6~^>?OeBF{LUlA&RD| z*2LR6+Nry6cDgaRj7F9NlDM^o9l@w9jG$7Z6+C@i4E)#oOTPF7HQ*k!s+;a-^M9IL zPrUDca$V^J%zWW@-SS1daNDO7G_0PQ9$valU7s1hAI*@I-Wnf)d@AciJ{@3x4DJV1 zI&ReQ!B1;>YQ&%YIe?;TyxiLQx&6&-A38A44N3Q@#Z-o)Z;=pqJG(M0MI(~4w;VMQ z5fxkQnCvlGZAjy#4%5(IT2kJcIcz+ar6%El`DyO7`F67Aq+8(u?X83Ee%|qtkG1U{ zxHD_<(~spxCMIfukH1Il(31`p1by9~3=K?2!q|II3zzm<*h9G`*N2MqBpwoi%|Gvy z9o&l!&D-WZ2+Y`-?%!>IHHwd%DJI5CNAnenLhCATGZlpB^0d9j=@z9Naop;KF?(27 zd~T3Ltw>q((__&V*<Ok>`|4ADy&4AU?Mw~9vJOEvWdgzUX=lagscAcn264pbEP8J4 zxT$|lt=~N1iCtHNww;QGb;tcru?wv(u)t(7eCAPUgvObvk~RrBsaDnVc&cHnV{Lr} zzu4JO8=t6aUQ#ivrDsbTKFEoX5+CFjmQ2c6KDLNw-Ze$iGK7W7rO+1rTv{%A=5h=} z=JAX5nZ71ro@}fhJaYH#v0FRnUiK7=e_T@eL)|6vQ^8V%o%nIa#Q+v5F#XDG)4%6` z`Uu1+E)b{O?TO#-?ABayd(vhoDx~v5o|_C72GDPiVFkm{OXneUSlh188IM@IWK$Y^ zv_3#CoQItMctN6FTLS2owgI3hCp_j~p^#5j!ItkHKbm6Wyi9Wp#@(OR;ddc4|9>;i zv2%Ot)`Vmk)ed(&HIGW9728#X128GQ6V|>uP0+1}Zg@%^#t{&@eyQb}R|Kx~l{H^S z2v|TFl`aX`#$LZUlYY-%GkC>@nL&!<rmy~R$}Mm|&=?WnxKj$UDy!>o<X1AJ`4xon zKHvk6mL_NSew~RaGAzDpf9c;rcKC(o!taN>ZnSbUJ}FKzkIP!^Y&Q+y$G+?07izPB ztp-f8AmpBs7!aN!W^pDl(^sGI4$$E`49dQXg(*=?YNHxeW;$ap6j%w>7!U{N1TqFj zmJY5j#}0;PS~lgCG3uFI><nIH6!L4-bT!j~J$~@!qwm$&>;H(Dp=(WF9|iBjJ+|Nx zaZOcyD#~42DotvQacc8V3m)a2KIzCSv2vCOJQssNXlUrv1S#p~Oo@<G*i?n~ExV@U zZV{VATr^VDu<wfPf>T=d^Ik6ympBd0-WSGl(oc|oB9qKl1XP7Q61l_FII}oo=VR>x z*}3aJjWQgU_(lrWKIRSaRe4g5@)h%?(F|VX{Odbql0j~}X|lvh|L5{;I)0&8J=xV{ zI8UK3g{sKD|JC?!WMT<mjsOmXZ-^>SC|KF0EMlml%w~|FfAH!-8%#JZE=U0y6QE^w z+p;snr686!#QuKQlnbNKOuXHWfc9nGT-$;amG@e9jI9eZZAdnXYYkEdN6PU{$KY=3 zFK}MEs<1+zwVRW5q7XrQm0MCa7drX5Vv@+3b#*-~_WT#iZ_A8u)-}o`sfWHrG=m~$ zKt7xhbbVMY^4@E})QF?drzFxc#y5+7d!J|i<<j+k9sMqNpR&IV!0CT57|0jo?M3Fb z^?#(XVpEj`{tgH|kn=5HqwNPu3OI@xHla^|fbU0xTxV{`{pxegXG0Y2Wog}SAdm}> zeelIzaD<!dQi&8mz!*N#nA2!w6X%LD8EM%+&;Z=8YUq$dJ3wxkBCFQ$#(_G;B^E=L z-q}@cnwxQma2vr|wQ|Cc<Gyx|ho}CC>3nf+8N5r<tmkVRJbhUN9(l?apw8?PKv6x~ zSIbmVq&bgFLWNm$gtuu6CmV#``DBrxc{P_F+zdc-{}Fnp$DxmDkI4Z~P2cl9h2bTY zJU|?4gKesiYwTgJVT16{rycd|4}uo~J0n=c%vVvBg`JcBQ+n35%>O<;c=O5fJFuef z^G>kV<WA2?{VMJ|a8I!BHSpk;7$1KC6a-L~_t`FGlI~4QldG|ppLR{VGYUm>bOyUH zs&YLN%>BKCeAl$E;^Pm=)G;wq^dB0sfRn#i;#3uU+Tk7vuERYtNlH<lBH|y_B&C~k zH@^LMve;{Y8bkj?feGX>s+JAnn(5Ea32{>v-TsS~lm0Vhl6p*h)9<g6FP%CDoV+u5 zi4p!kA38pDT)M)oLk1H6uYmRk_iZxT*Z|*%C<zccoQru5K;akR-?})TNBbT!js#cS za|o%nv<!zp7#ie3I;8A-@tTtB)He_kKh~}Os4Wt#uARrTn22)}eB|2BnL%S>+lrQF z{7$&0j=aY)b`=)2ZL)%5<DRR9jXVZ;;iVE8ZMX$@gI2_r>fF9vMXfLD=d$~H5fGXh zABFd#iVWQNgcD_`-r@Mlh6@Y~RjsBh#H{F=AD!40aSzzbhMVMace*|75q|>e1=9l5 zG*~3&&8(l}$T2J4xxu=^R{meog^4`a2E<OuDFTQN`~&1S`vR<!5%db@axXj=_(I<F ze=neh6K4U0_BllCx2dQey_CQ7?VK1R)zc5;B#ZnxXTVGVJpv_YFOa)H?HWk(F+w0+ zjka9{4wG1rkfBL86v+fugjByu5ear1wb8q|5LUv3XXjP`YJRehbjc0H$zD2<f_!Es z=DyyDnweuv11e@@$Q2=V^Cb5mYkqbszosgk40M!D7v~&9C9Q9vBYM54YI&Wy6QHWT z4(MArhA!Eg|J#fY|1~2qRP>6&za|_o_yT0AirBByhLj(~yhpL?md{nxvO&j=?hou3 z2^}FNU6M*5oFK$QC@$aW;p+m8#Hyr-0_pGwFaUn@8ZHfkjo)s_lluV(m3&PiH-zCl zFsVHF2>BpJu0so9dO1w-LG1kU1;A?{ubl&Yo8N07X$9>0@4gW3i`-;V$6fF}GFMOt z9MIw4p!1s|c>S*Yd2;=Et}MQ1s!;pJkjM#RF{2`nhlkAfjOH0{L)c$H1YTH2e!Mf1 zJn|NE1NzwbzSWfviWCCMZnV6ix7JN5XsuoJY3jv}l8;QMyo$hiz+&qp4Ha04Sa&pm zD(S0r`Y$Sz?Yr>5>>K@m?AurBc~aOY@UNSu>{-!wX$C$>ZJU7NoYZP36bnc2podNR z_O7J&cKFK+jII}D)e#l`80ksJisIa_|9YJw1uaP1jeZ^Be>ZJi(IN~4!vGzGY{cLG z2$aLf76awnmvw;u0Ll;KF8BzXl3YFPf6Pbq|9<%Gh2$NEDL#TS>@NXh08mGOdx5MV z1P+4re+~LK*!Dc+#>;EKuIF!wncaC;oyA?sm~UM>G~Zk7S)CvAuxGo;>=`Iqg%;($ zLrNYgGXBqq(06yVt2w^4Idt*h(h5_+?)uE2T>JY!{Bn;j>`al*4!NM@H74vO<Mclv zec=%zUhbCvNxGOPj&?h7plz0*TgviNXy>GdGL<sfVpb{JWd_)gJW|Pa!?+Vn>Y?r_ zmHJizx-RU-Vh{?E*(?63ji?OPzI_6=x&Y|_YyeDi<5h64rxg{F3KT`9jAZ=yHv|y; zz~Bp90<asm&?|_*hu@Y2ESgJQ<XA@bH^@u!HU57VV9&SYRt&7f^iJ*3OR)KWtOgkd z$E1=6f4T<ps@#yfujD^~&7kQ0`D>pBUNooFj`^#_0Hc}HwLQhtAycX;e?Y8mep~p0 z*Q<7G!BbflXfocrLuYzI$=MsDe?8Wk(cy1%a|mQ#d-w9fH*hGhQ1KJ&+Qy6PM;f6n zBO~_k7r!7pf!qTfL>$URFbmyLk#8;QyvX=NQbJOsy#uXEY!&NLX(GGa&j~%?a%psh z<-p_MoE862T6O$!D49->RYO-I+;j$id<Mt2zjR(mVz$?{k7<;tfZVl>_S_{~irjL+ zW^V9v0rYTWvKe`yfBwo90@zl7wfL`}k$3!Wc>uU2FUdB6T>1uKq>@h|OHKS|E^<Tt zy@{Rtr`#{(qhDb+`#%Od=X_57Ki01AwhS{=Z_P6+9m+N15C+U<ZbNC+&B!rhbF<0J z<mc<7zw^Vj9oj*?m1R&e4RUThXOX7=CHc5ud02_cVRmiX*E_+t>+9yaydk$#C3dt) zc}wNiA>D{W`?fpf>Yda(|KnYA9Mjy5xB8qM^O2R=eX)9DtJu6EDm7G?fP832&mMl; zA)w|sr7hvxwdatFfHhSBkF?hNvsE%lwe)Z2XNtKte-MQe{(^jfyacy+7eevjIk|Hn zU&=Y555T;`<W&ghEQarjkk^DjTLZg*g={Iv^_PI%EOsvMy#1|V;6-3bdHHXv{01TS zApe=|12{CW>fNZf<nHRc$ls82kPpC@A&M>~nj&FMYVgetU*>yj+KTq0C#sp9Vy>;r zHk2GO&$+?#xiF#e4_gM<`QK%werafr$7A1!1YV1=eps5--RbOfoIbUw6swK)u?3jN zlM_6tS5MsU4?2?#I&dFq_Ok#DEwUe#!P!o3XLb&dbI%}(3g$glX6=HJO@@y{5RAd4 zq2#F5@;*=g{4Groh{;Xvw-fz)zd$=y{=X|Y_Xq+3{oTJ70Jrm!yn2Q53i*b>LME3% z&b<I@;D5;PW_cS}33$voGxEMb|LWzHOD})-$siDkG&8#CIpo7D{_JEnD}smorr%xo Hh5vs5{BvNq literal 0 HcmV?d00001 diff --git a/nginx.conf.sample b/nginx.conf.sample index 296f9fafd0a35..2dbba68c39c39 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -145,6 +145,7 @@ location /media/ { # # # Replace placeholders and uncomment the line below to serve product images from public S3 # # See examples of S3 authentication at https://github.com/anomalizer/ngx_aws_auth +# # resolver 8.8.8.8; # # proxy_pass https://<bucket-name>.<region-name>.amazonaws.com; # # set $width "-"; diff --git a/pub/get.php b/pub/get.php index 215a83b74fbca..c59365c98727c 100644 --- a/pub/get.php +++ b/pub/get.php @@ -43,13 +43,16 @@ // Serve file if it's materialized if ($mediaDirectory) { - if (!$isAllowed($relativePath, $allowedResources)) { + $fileAbsolutePath = __DIR__ . '/' . $relativePath; + $fileRelativePath = str_replace(rtrim($mediaDirectory, '/') . '/', '', $fileAbsolutePath); + + if (!$isAllowed($fileRelativePath, $allowedResources)) { require_once 'errors/404.php'; exit; } - $mediaAbsPath = $mediaDirectory . '/' . $relativePath; - if (is_readable($mediaAbsPath)) { - if (is_dir($mediaAbsPath)) { + + if (is_readable($fileAbsolutePath)) { + if (is_dir($fileAbsolutePath)) { require_once 'errors/404.php'; exit; } @@ -57,7 +60,7 @@ new \Magento\Framework\HTTP\PhpEnvironment\Response(), new \Magento\Framework\File\Mime() ); - $transfer->send($mediaAbsPath); + $transfer->send($fileAbsolutePath); exit; } } diff --git a/pub/media/sitemap/.htaccess b/pub/media/sitemap/.htaccess new file mode 100644 index 0000000000000..b97408bad3f2e --- /dev/null +++ b/pub/media/sitemap/.htaccess @@ -0,0 +1,7 @@ +<IfVersion < 2.4> + order allow,deny + deny from all +</IfVersion> +<IfVersion >= 2.4> + Require all denied +</IfVersion> From 264f5b0c150ade48445ee6df1687f6a203e11ac1 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Sun, 25 Oct 2020 17:55:07 -0500 Subject: [PATCH 121/195] MC-38518: B2B - Qty update on cart - Page load time is high - Fix repetitive database queries in checkout process --- .../Model/StockRegistryPreloader.php | 131 ++++++++++++++++++ .../Observer/AddStockItemsObserver.php | 39 +++--- .../Observer/AddStockItemsObserverTest.php | 75 +++------- .../Magento/Quote/Model/QuoteRepository.php | 16 ++- .../Test/Unit/Model/QuoteRepositoryTest.php | 71 ++++++++-- .../ReorderConfigurableWithVariationsTest.php | 7 +- ...ingPreloadedStockDataInToStockRegistry.php | 21 +++ .../etc/di.xml | 12 ++ .../etc/module.xml | 10 ++ .../registration.php | 13 ++ 10 files changed, 296 insertions(+), 99 deletions(-) create mode 100644 app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php create mode 100644 dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php create mode 100644 dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml create mode 100644 dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/module.xml create mode 100644 dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php b/app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php new file mode 100644 index 0000000000000..2d0aef46a4ebd --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; + +/** + * Preload stock data into stock registry + */ +class StockRegistryPreloader +{ + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + /** + * @var StockRegistryStorage + */ + private $stockRegistryStorage; + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $stockItemCriteriaFactory; + /** + * @var StockStatusCriteriaInterfaceFactory + */ + private $stockStatusCriteriaFactory; + /** + * @var StockStatusRepositoryInterface + */ + private $stockStatusRepository; + + /** + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockStatusRepositoryInterface $stockStatusRepository + * @param StockItemCriteriaInterfaceFactory $stockItemCriteriaFactory + * @param StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory + * @param StockConfigurationInterface $stockConfiguration + * @param StockRegistryStorage $stockRegistryStorage + */ + public function __construct( + StockItemRepositoryInterface $stockItemRepository, + StockStatusRepositoryInterface $stockStatusRepository, + StockItemCriteriaInterfaceFactory $stockItemCriteriaFactory, + StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory, + StockConfigurationInterface $stockConfiguration, + StockRegistryStorage $stockRegistryStorage + ) { + $this->stockItemRepository = $stockItemRepository; + $this->stockStatusRepository = $stockStatusRepository; + $this->stockItemCriteriaFactory = $stockItemCriteriaFactory; + $this->stockStatusCriteriaFactory = $stockStatusCriteriaFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockRegistryStorage = $stockRegistryStorage; + } + + /** + * Preload stock item into stock registry + * + * @param array $productIds + * @param int|null $scopeId + * @return \Magento\CatalogInventory\Api\Data\StockItemInterface[] + */ + public function preloadStockItems(array $productIds, ?int $scopeId = null): array + { + $scopeId = $scopeId ?? $this->stockConfiguration->getDefaultScopeId(); + $criteria = $this->stockItemCriteriaFactory->create(); + $criteria->setProductsFilter($productIds); + $criteria->setScopeFilter($scopeId); + $collection = $this->stockItemRepository->getList($criteria); + $this->setStockItems($collection->getItems(), $scopeId); + return $collection->getItems(); + } + + /** + * Saves stock items into registry + * + * @param \Magento\CatalogInventory\Api\Data\StockItemInterface[] $stockItems + * @param int $scopeId + */ + public function setStockItems(array $stockItems, int $scopeId): void + { + foreach ($stockItems as $item) { + $this->stockRegistryStorage->setStockItem($item->getProductId(), $scopeId, $item); + } + } + + /** + * Preload stock status into stock registry + * + * @param array $productIds + * @param int|null $scopeId + * @return \Magento\CatalogInventory\Api\Data\StockStatusInterface[] + */ + public function preloadStockStatuses(array $productIds, ?int $scopeId = null): array + { + $scopeId = $scopeId ?? $this->stockConfiguration->getDefaultScopeId(); + $criteria = $this->stockStatusCriteriaFactory->create(); + $criteria->setProductsFilter($productIds); + $criteria->setScopeFilter($scopeId); + $collection = $this->stockStatusRepository->getList($criteria); + $this->setStockStatuses($collection->getItems(), $scopeId); + return $collection->getItems(); + } + + /** + * Saves stock statuses into registry + * + * @param \Magento\CatalogInventory\Api\Data\StockStatusInterface[] $stockStatuses + * @param int $scopeId + */ + public function setStockStatuses(array $stockStatuses, int $scopeId): void + { + foreach ($stockStatuses as $item) { + $this->stockRegistryStorage->setStockStatus($item->getProductId(), $scopeId, $item); + } + } +} diff --git a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php index 8fa90cf6531c4..68924c635de9d 100644 --- a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php @@ -10,7 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; @@ -19,36 +19,27 @@ */ class AddStockItemsObserver implements ObserverInterface { - /** - * @var StockItemCriteriaInterfaceFactory - */ - private $criteriaInterfaceFactory; - - /** - * @var StockItemRepositoryInterface - */ - private $stockItemRepository; - /** * @var StockConfigurationInterface */ private $stockConfiguration; + /** + * @var StockRegistryPreloader + */ + private $stockRegistryPreloader; /** * AddStockItemsObserver constructor. * - * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory - * @param StockItemRepositoryInterface $stockItemRepository * @param StockConfigurationInterface $stockConfiguration + * @param StockRegistryPreloader $stockRegistryPreloader */ public function __construct( - StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, - StockItemRepositoryInterface $stockItemRepository, - StockConfigurationInterface $stockConfiguration + StockConfigurationInterface $stockConfiguration, + StockRegistryPreloader $stockRegistryPreloader ) { - $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; - $this->stockItemRepository = $stockItemRepository; $this->stockConfiguration = $stockConfiguration; + $this->stockRegistryPreloader = $stockRegistryPreloader; } /** @@ -62,11 +53,13 @@ public function execute(Observer $observer) /** @var Collection $productCollection */ $productCollection = $observer->getData('collection'); $productIds = array_keys($productCollection->getItems()); - $criteria = $this->criteriaInterfaceFactory->create(); - $criteria->setProductsFilter($productIds); - $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - foreach ($stockItemCollection->getItems() as $item) { + $scopeId = $this->stockConfiguration->getDefaultScopeId(); + $stockItems = []; + if ($productIds) { + $stockItems = $this->stockRegistryPreloader->preloadStockItems($productIds, $scopeId); + $this->stockRegistryPreloader->preloadStockStatuses($productIds, $scopeId); + } + foreach ($stockItems as $item) { /** @var Product $product */ $product = $productCollection->getItemById($item->getProductId()); $productExtension = $product->getExtensionAttributes(); diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php index bba44ef436fd6..fce232821b67d 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php @@ -10,15 +10,12 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\CatalogInventory\Api\Data\StockItemCollectionInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockItemCriteriaInterface; use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; use Magento\CatalogInventory\Observer\AddStockItemsObserver; use Magento\Framework\Event\Observer; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -33,46 +30,29 @@ class AddStockItemsObserverTest extends TestCase * @var AddStockItemsObserver */ private $subject; - /** - * @var StockItemCriteriaInterfaceFactory|MockObject - */ - private $criteriaInterfaceFactoryMock; - - /** - * @var StockItemRepositoryInterface|MockObject - */ - private $stockItemRepositoryMock; /** * @var StockConfigurationInterface|MockObject */ private $stockConfigurationMock; + /** + * @var StockRegistryPreloader|MockObject + */ + private $stockRegistryPreloader; /** * @inheritdoc */ protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->criteriaInterfaceFactoryMock = $this->getMockBuilder(StockItemCriteriaInterfaceFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->stockItemRepositoryMock = $this->getMockBuilder(StockItemRepositoryInterface::class) - ->setMethods(['getList']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) ->setMethods(['getDefaultScopeId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->subject = $objectManager->getObject( - AddStockItemsObserver::class, - [ - 'criteriaInterfaceFactory' => $this->criteriaInterfaceFactoryMock, - 'stockItemRepository' => $this->stockItemRepositoryMock, - 'stockConfiguration' => $this->stockConfigurationMock - ] + $this->stockRegistryPreloader = $this->createMock(StockRegistryPreloader::class); + $this->subject = new AddStockItemsObserver( + $this->stockConfigurationMock, + $this->stockRegistryPreloader, ); } @@ -84,26 +64,6 @@ public function testExecute() $productId = 1; $defaultScopeId = 0; - $criteria = $this->getMockBuilder(StockItemCriteriaInterface::class) - ->setMethods(['setProductsFilter', 'setScopeFilter']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $criteria->expects(self::once()) - ->method('setProductsFilter') - ->with(self::identicalTo([$productId])) - ->willReturn(true); - $criteria->expects(self::once()) - ->method('setScopeFilter') - ->with(self::identicalTo($defaultScopeId)) - ->willReturn(true); - - $this->criteriaInterfaceFactoryMock->expects(self::once()) - ->method('create') - ->willReturn($criteria); - $stockItemCollection = $this->getMockBuilder(StockItemCollectionInterface::class) - ->setMethods(['getItems']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); $stockItem = $this->getMockBuilder(StockItemInterface::class) ->setMethods(['getProductId']) ->disableOriginalConstructor() @@ -112,14 +72,19 @@ public function testExecute() ->method('getProductId') ->willReturn($productId); - $stockItemCollection->expects(self::once()) - ->method('getItems') + $this->stockRegistryPreloader->expects(self::once()) + ->method('preloadStockItems') + ->with([$productId]) ->willReturn([$stockItem]); - $this->stockItemRepositoryMock->expects(self::once()) - ->method('getList') - ->with(self::identicalTo($criteria)) - ->willReturn($stockItemCollection); + $this->stockRegistryPreloader->expects(self::once()) + ->method('preloadStockStatuses') + ->with([$productId]) + ->willReturn([]); + + $this->stockRegistryPreloader->expects(self::once()) + ->method('preloadStockItems') + ->willReturn([$stockItem]); $this->stockConfigurationMock->expects(self::once()) ->method('getDefaultScopeId') diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index 0dd2b00a596ea..1533194023e3e 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -25,7 +25,7 @@ use Magento\Store\Model\StoreManagerInterface; /** - * Quote repository. + * Repository for quote entity. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -146,10 +146,16 @@ public function get($cartId, array $sharedStoreIds = []) public function getForCustomer($customerId, array $sharedStoreIds = []) { if (!isset($this->quotesByCustomerId[$customerId])) { - $quote = $this->loadQuote('loadByCustomer', 'customerId', $customerId, $sharedStoreIds); - $this->getLoadHandler()->load($quote); - $this->quotesById[$quote->getId()] = $quote; - $this->quotesByCustomerId[$customerId] = $quote; + $customerQuote = $this->loadQuote('loadByCustomer', 'customerId', $customerId, $sharedStoreIds); + $customerQuoteId = $customerQuote->getId(); + //prevent loading quote items for same quote + if (isset($this->quotesById[$customerQuoteId])) { + $customerQuote = $this->quotesById[$customerQuoteId]; + } else { + $this->getLoadHandler()->load($customerQuote); + } + $this->quotesById[$customerQuoteId] = $customerQuote; + $this->quotesByCustomerId[$customerId] = $customerQuote; } return $this->quotesByCustomerId[$customerId]; } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php index e19fb93255eb2..add00ba71d0fb 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php @@ -33,6 +33,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyMethods) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class QuoteRepositoryTest extends TestCase { @@ -223,14 +224,44 @@ public function testGet() static::assertEquals($this->quoteMock, $this->model->get($cartId)); } - public function testGetForCustomerAfterGet() + /** + * @param int $quoteId + * @param int $customerQuoteId + * @param bool $isSame + * @dataProvider getForCustomerAfterGetDataProvider + */ + public function testGetForCustomerAfterGet(int $quoteId, int $customerQuoteId, bool $isSame) { - $cartId = 15; $customerId = 23; + $customerQuote = $this->getMockBuilder(Quote::class) + ->addMethods( + [ + 'setSharedStoreIds', + 'getCustomerId' + ] + ) + ->onlyMethods( + [ + 'load', + 'loadByIdWithoutStore', + 'loadByCustomer', + 'getIsActive', + 'getId', + 'save', + 'delete', + 'getStoreId', + 'getData' + ] + ) + ->disableOriginalConstructor() + ->getMock(); $this->cartFactoryMock->expects(static::exactly(2)) ->method('create') - ->willReturn($this->quoteMock); + ->willReturnOnConsecutiveCalls( + $this->quoteMock, + $customerQuote + ); $this->storeManagerMock->expects(static::exactly(2)) ->method('getStore') ->willReturn($this->storeMock); @@ -241,24 +272,34 @@ public function testGetForCustomerAfterGet() ->method('setSharedStoreIds'); $this->quoteMock->expects(static::once()) ->method('loadByIdWithoutStore') - ->with($cartId) - ->willReturn($this->storeMock); - $this->quoteMock->expects(static::once()) + ->with($quoteId) + ->willReturnSelf(); + $customerQuote->expects(static::once()) ->method('loadByCustomer') ->with($customerId) - ->willReturn($this->storeMock); - $this->quoteMock->expects(static::exactly(3)) - ->method('getId') - ->willReturn($cartId); - $this->quoteMock->expects(static::any()) - ->method('getCustomerId') + ->willReturnSelf(); + $this->quoteMock->method('getId') + ->willReturn($quoteId); + $customerQuote->method('getId') + ->willReturn($customerQuoteId); + $this->quoteMock->method('getCustomerId') + ->willReturn($customerId); + $customerQuote->method('getCustomerId') ->willReturn($customerId); - $this->loadHandlerMock->expects(static::exactly(2)) + $this->loadHandlerMock->expects($isSame ? $this->once() : $this->exactly(2)) ->method('load') ->with($this->quoteMock); - static::assertEquals($this->quoteMock, $this->model->get($cartId)); - static::assertEquals($this->quoteMock, $this->model->getForCustomer($customerId)); + static::assertSame($this->quoteMock, $this->model->get($quoteId)); + static::assertSame($isSame ? $this->quoteMock : $customerQuote, $this->model->getForCustomer($customerId)); + } + + public function getForCustomerAfterGetDataProvider(): array + { + return [ + [15, 15, true], + [15, 16, false], + ]; } public function testGetWithSharedStoreIds() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php index d29187dc7986d..5bd01e8eaff20 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php @@ -7,10 +7,12 @@ namespace Magento\GraphQl\Sales; +use Magento\CatalogInventory\Model\StockRegistryStorage; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -73,7 +75,6 @@ public function testVariations() $productSku = 'simple_20'; /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get($productSku); - $this->assertValidVariations(); $this->assertWithOutOfStockVariation($productRepository, $product); } @@ -141,6 +142,10 @@ private function assertWithOutOfStockVariation( \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Catalog\Api\Data\ProductInterface $product ): void { + /** @var $stockRegistryStorage StockRegistryStorage */ + $stockRegistryStorage = Bootstrap::getObjectManager()->get(StockRegistryStorage::class); + // clean stock registry + $stockRegistryStorage->clean(); // make product available in stock but disable and make reorder $product->setStockData( [ diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php new file mode 100644 index 0000000000000..95705afb0c5a8 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleCatalogInventoryCache\Plugin; + +class PreventCachingPreloadedStockDataInToStockRegistry +{ + public function aroundSetStockItems(): void + { + //do not cache + } + + public function aroundSetStockStatuses(): void + { + //do not cache + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml new file mode 100644 index 0000000000000..d539c3aad158d --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\CatalogInventory\Model\StockRegistryPreloader"> + <plugin name="prevent_caching_preloaded_stock_data" type="Magento\TestModuleCatalogInventoryCache\Plugin\PreventCachingPreloadedStockDataInToStockRegistry"/> + </type> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/module.xml new file mode 100644 index 0000000000000..4446f4186d30c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/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_TestModuleCatalogInventoryCache" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php new file mode 100644 index 0000000000000..15279c9839dd2 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogInventoryCache') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogInventoryCache', __DIR__); +} From d5fdbe6adb48e14a0388489c9638f80548fee53f Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Wed, 28 Oct 2020 16:45:57 -0500 Subject: [PATCH 122/195] MC-37933: Product Export File Does Not Show In Admin - refactored --- .../Model/Export/RowCustomizer.php | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php index f24b27389215a..5dc98f2d150f4 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php @@ -83,37 +83,32 @@ public function prepareData($collection, $productIds): void // set global scope during export $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); - $collection->setPageSize(100); - $pages = $collection->getLastPageNumber(); - for ($pageNum = 1; $pageNum <= $pages; $pageNum++) { - $collection->setCurPage($pageNum); - foreach ($collection as $product) { - $productLinks = $this->linkRepository->getLinksByProduct($product); - $productSamples = $this->sampleRepository->getSamplesByProduct($product); - $this->downloadableData[$product->getId()] = []; - $linksData = []; - $samplesData = []; - foreach ($productLinks as $linkId => $link) { - $linkData = $link->getData(); - $linkData['group_title'] = $product->getData('links_title'); - $linksData[$linkId] = $this->optionRowToCellString($linkData); - } - foreach ($productSamples as $sampleId => $sample) { - $sampleData = $sample->getData(); - $sampleData['group_title'] = $product->getData('samples_title'); - $samplesData[$sampleId] = $this->optionRowToCellString($sampleData); - } - $this->downloadableData[$product->getId()] = [ - Downloadable::COL_DOWNLOADABLE_LINKS => implode( - ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, - $linksData - ), - Downloadable::COL_DOWNLOADABLE_SAMPLES => implode( - Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, - $samplesData - )]; + while ($product = $productCollection->fetchItem()) { + /** @var $product \Magento\Catalog\Api\Data\ProductInterface */ + $productLinks = $this->linkRepository->getLinksByProduct($product); + $productSamples = $this->sampleRepository->getSamplesByProduct($product); + $this->downloadableData[$product->getId()] = []; + $linksData = []; + $samplesData = []; + foreach ($productLinks as $linkId => $link) { + $linkData = $link->getData(); + $linkData['group_title'] = $product->getData('links_title'); + $linksData[$linkId] = $this->optionRowToCellString($linkData); } - $collection->clear(); + foreach ($productSamples as $sampleId => $sample) { + $sampleData = $sample->getData(); + $sampleData['group_title'] = $product->getData('samples_title'); + $samplesData[$sampleId] = $this->optionRowToCellString($sampleData); + } + $this->downloadableData[$product->getId()] = [ + Downloadable::COL_DOWNLOADABLE_LINKS => implode( + ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, + $linksData + ), + Downloadable::COL_DOWNLOADABLE_SAMPLES => implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + $samplesData + )]; } } From 9c107b0e725d277e3e47b90367a25ec7a176b47e Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Wed, 28 Oct 2020 17:41:55 -0500 Subject: [PATCH 123/195] MC-38518: B2B - Qty update on cart - Page load time is high - Add unit test --- .../Unit/Model/StockRegistryPreloaderTest.php | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php new file mode 100644 index 0000000000000..037d491c8b5bf --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Test\Unit\Model; + +use Magento\CatalogInventory\Api\Data\StockItemCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; +use Magento\CatalogInventory\Model\StockRegistryStorage; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for StockRegistryStorage + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class StockRegistryPreloaderTest extends TestCase +{ + /** + * @var StockItemRepositoryInterface|MockObject + */ + private $stockItemRepository; + /** + * @var StockStatusRepositoryInterface|MockObject + */ + private $stockStatusRepository; + /** + * @var MockObject + */ + private $stockItemCriteriaFactory; + /** + * @var MockObject + */ + private $stockStatusCriteriaFactory; + /** + * @var StockConfigurationInterface|MockObject + */ + private $stockConfiguration; + /** + * @var StockRegistryStorage + */ + private $stockRegistryStorage; + /** + * @var StockRegistryPreloader + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->stockItemRepository = $this->createMock(StockItemRepositoryInterface::class); + $this->stockStatusRepository = $this->createMock(StockStatusRepositoryInterface::class); + $this->stockItemCriteriaFactory = $this->createMock(StockItemCriteriaInterfaceFactory::class); + $this->stockStatusCriteriaFactory = $this->createMock(StockStatusCriteriaInterfaceFactory::class); + $this->stockConfiguration = $this->createMock(StockConfigurationInterface::class); + $this->stockRegistryStorage = new StockRegistryStorage(); + $this->model = new StockRegistryPreloader( + $this->stockItemRepository, + $this->stockStatusRepository, + $this->stockItemCriteriaFactory, + $this->stockStatusCriteriaFactory, + $this->stockConfiguration, + $this->stockRegistryStorage, + ); + } + + public function testPreloadStockItems(): void + { + $productIds = [10, 20]; + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 20]), + ]; + $collection = $this->createConfiguredMock(StockItemCollectionInterface::class, ['getItems' => $stockItems]); + $criteria = $this->createMock(StockItemCriteriaInterface::class); + $criteria->expects($this->once()) + ->method('setProductsFilter') + ->with($productIds) + ->willReturnSelf(); + $criteria->expects($this->once()) + ->method('setScopeFilter') + ->with($scopeId) + ->willReturnSelf(); + $this->stockItemRepository->method('getList') + ->willReturn($collection); + $this->stockItemCriteriaFactory->method('create') + ->willReturn($criteria); + $this->assertEquals($stockItems, $this->model->preloadStockItems($productIds, $scopeId)); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockItem(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockItem(20, $scopeId)); + } + + public function testPreloadStockStatuses(): void + { + $productIds = [10, 20]; + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 20]), + ]; + $collection = $this->createConfiguredMock(StockStatusCollectionInterface::class, ['getItems' => $stockItems]); + $criteria = $this->createMock(StockStatusCriteriaInterface::class); + $criteria->expects($this->once()) + ->method('setProductsFilter') + ->with($productIds) + ->willReturnSelf(); + $criteria->expects($this->once()) + ->method('setScopeFilter') + ->with($scopeId) + ->willReturnSelf(); + $this->stockStatusRepository->method('getList') + ->willReturn($collection); + $this->stockStatusCriteriaFactory->method('create') + ->willReturn($criteria); + $this->assertEquals($stockItems, $this->model->preloadStockStatuses($productIds, $scopeId)); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockStatus(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockStatus(20, $scopeId)); + } + + public function testSetStockItems(): void + { + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 20]), + ]; + $this->model->setStockItems($stockItems, $scopeId); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockItem(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockItem(20, $scopeId)); + } + + public function testSetStockStatuses(): void + { + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 20]), + ]; + $this->model->setStockStatuses($stockItems, $scopeId); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockStatus(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockStatus(20, $scopeId)); + } +} From 32a2d7d199bf0e1c1c1234b8464344bdc62ad16f Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Thu, 29 Oct 2020 01:32:05 -0500 Subject: [PATCH 124/195] MC-37933: Product Export File Does Not Show In Admin - modified test --- .../Unit/Model/Export/RowCustomizerTest.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php index 27f3d62bd90ec..f36676c1a8749 100644 --- a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php +++ b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php @@ -92,8 +92,8 @@ public function testPrepareData() ->disableOriginalConstructor() ->getMock(); $collection->expects($this->atLeastOnce()) - ->method('getIterator') - ->willReturn(new \ArrayIterator([$product1, $product2])); + ->method('fetchItem') + ->willReturn($product1, $product2); $collection->expects($this->exactly(2)) ->method('addAttributeToFilter') @@ -101,19 +101,6 @@ public function testPrepareData() $collection->expects($this->exactly(2)) ->method('addAttributeToSelect') ->willReturnSelf(); - $collection->expects($this->once()) - ->method('setPageSize') - ->willReturnSelf(); - $collection->expects($this->once()) - ->method('getLastPageNumber') - ->willReturn(1); - $collection->expects($this->atLeastOnce()) - ->method('setCurPage') - ->willReturnSelf(); - $collection->expects($this->once()) - ->method('clear') - ->willReturnSelf(); - $this->linkRepositoryMock->expects($this->exactly(2)) ->method('getLinksByProduct') ->will($this->returnValue([])); From 7adea3a008c8cb16bc5099ae8c59ed290db70d74 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Thu, 29 Oct 2020 14:55:29 +0200 Subject: [PATCH 125/195] MC-38050: Price Sorting Is not working --- .../Magento/Bundle/Model/ResourceModel/Indexer/Price.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index 96d68d7e74117..55bef4980098b 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -17,6 +17,7 @@ use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Model\Stock; /** * Bundle products Price indexer resource model @@ -624,6 +625,13 @@ private function calculateDynamicBundleSelectionPrice($dimensions) 'tier_price' => $tierExpr, ] ); + $select->join( + ['si' => $this->getTable('cataloginventory_stock_status')], + 'si.product_id = bs.product_id', + [] + ); + $select->where('si.stock_status = ?', Stock::STOCK_IN_STOCK); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); } From 645cf2323e1c44af0b8badc51e3af647e988a31d Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 29 Oct 2020 15:09:59 +0200 Subject: [PATCH 126/195] MC-37093: Create automated test for "Collect data default data definition by cron" --- .../Analytics/Cron/CollectDataTest.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php new file mode 100644 index 0000000000000..227474edfc2e1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Analytics\Cron; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks data collection process behaviour + * + * @see \Magento\Analytics\Cron\CollectData + * + * @magentoAppArea adminhtml + */ +class CollectDataTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CollectData */ + private $collectDataService; + + /** @var Filesystem */ + private $fileSystem; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->collectDataService = $this->objectManager->get(CollectData::class); + $this->fileSystem = $this->objectManager->get(Filesystem::class); + $this->removeAnalyticsDirectory(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->removeAnalyticsDirectory(); + + parent::tearDown(); + } + + /** + * @magentoConfigFixture current_store analytics/subscription/enabled 1 + * @magentoConfigFixture default/analytics/general/token 123 + * + * @return void + */ + public function testExecute(): void + { + $this->collectDataService->execute(); + $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->assertTrue( + $mediaDirectory->isDirectory('analytics'), + 'Analytics was not created' + ); + $files = $mediaDirectory->getDriver()->readDirectoryRecursively($mediaDirectory->getAbsolutePath('analytics')); + $file = array_filter($files, function ($element) { + return substr($element, -8) === 'data.tgz'; + }); + $this->assertNotEmpty($file, 'File was not created'); + } + + /** + * Remove Analytics directory + * + * @return void + */ + private function removeAnalyticsDirectory(): void + { + $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); + $directoryToRemove = $mediaDirectory->getAbsolutePath('analytics'); + if ($mediaDirectory->isDirectory($directoryToRemove)) { + $mediaDirectory->delete($directoryToRemove); + } + } +} From cf5e1563a1290ad638b22511666a3af53a4a4ae2 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 30 Oct 2020 09:53:31 +0200 Subject: [PATCH 127/195] MC-35740: Using API to capture payment --- app/code/Magento/Sales/etc/webapi_soap/di.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 1a8478438b04a..12ad410279a08 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -19,7 +19,7 @@ </argument> </arguments> </type> - <type name="Magento\Sales\Model\Order\ShipmentRepository"> - <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> </type> </config> From 9a011bf016020c58a1c77737a153301cc33d93a6 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 30 Oct 2020 12:30:14 +0200 Subject: [PATCH 128/195] MC-37093: Create automated test for "Collect data default data definition by cron" --- .../Analytics/Cron/CollectDataTest.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php index 227474edfc2e1..a1d2b24d54b0e 100644 --- a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php +++ b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -28,8 +29,8 @@ class CollectDataTest extends TestCase /** @var CollectData */ private $collectDataService; - /** @var Filesystem */ - private $fileSystem; + /** @var WriteInterface */ + private $mediaDirectory; /** * @inheritdoc @@ -40,7 +41,7 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->collectDataService = $this->objectManager->get(CollectData::class); - $this->fileSystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); $this->removeAnalyticsDirectory(); } @@ -63,12 +64,12 @@ protected function tearDown(): void public function testExecute(): void { $this->collectDataService->execute(); - $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); $this->assertTrue( - $mediaDirectory->isDirectory('analytics'), + $this->mediaDirectory->isDirectory('analytics'), 'Analytics was not created' ); - $files = $mediaDirectory->getDriver()->readDirectoryRecursively($mediaDirectory->getAbsolutePath('analytics')); + $files = $this->mediaDirectory->getDriver() + ->readDirectoryRecursively($this->mediaDirectory->getAbsolutePath('analytics')); $file = array_filter($files, function ($element) { return substr($element, -8) === 'data.tgz'; }); @@ -82,10 +83,9 @@ public function testExecute(): void */ private function removeAnalyticsDirectory(): void { - $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); - $directoryToRemove = $mediaDirectory->getAbsolutePath('analytics'); - if ($mediaDirectory->isDirectory($directoryToRemove)) { - $mediaDirectory->delete($directoryToRemove); + $directoryToRemove = $this->mediaDirectory->getAbsolutePath('analytics'); + if ($this->mediaDirectory->isDirectory($directoryToRemove)) { + $this->mediaDirectory->delete($directoryToRemove); } } } From 7cd05c324b61dddaced8467c69780a59edbe50f7 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Fri, 30 Oct 2020 12:38:04 +0200 Subject: [PATCH 129/195] MC-38050: Price Sorting Is not working --- .../Model/ResourceModel/Indexer/PriceTest.php | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php new file mode 100644 index 0000000000000..afb0e66558aaa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Model\ResourceModel\Indexer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Price; +use Magento\Customer\Model\Group; +use Magento\Framework\Indexer\ActionInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Catalog\Model\Product\Price\GetPriceIndexDataByProductId; +use Magento\CatalogInventory\Model\Indexer\Stock; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class PriceTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ActionInterface + */ + private $indexer; + + /** + * @var GetPriceIndexDataByProductId + */ + private $getPriceIndexDataByProductId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var Stock + */ + private $stockIndexer; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->indexer = $this->objectManager->get(Price::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->getPriceIndexDataByProductId = $this->objectManager->get(GetPriceIndexDataByProductId::class); + $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->stockIndexer = $this->objectManager->get(Stock::class); + } + + /** + * Test get bundle index price if enabled show out off stock + * + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Bundle/_files/bundle_product_with_dynamic_price.php + * @magentoConfigFixture default_store cataloginventory/options/show_out_of_stock 1 + * + * @return void + */ + public function testExecuteRowWithShowOutOfStock(): void + { + + $expectedPrices = [ + 'price' => 0, + 'final_price' => 0, + 'min_price' => 15.99, + 'max_price' => 15.99, + 'tier_price' => null + ]; + $product = $this->productRepository->get('simple1'); + $product->setStockData(['qty' => 0]); + $this->productRepository->save($product); + $this->stockIndexer->executeRow($product->getId()); + $bundleProduct = $this->productRepository->get('bundle_product_with_dynamic_price'); + $this->indexer->executeRow($bundleProduct->getId()); + $this->assertIndexTableData($bundleProduct->getId(), $expectedPrices); + } + + /** + * Asserts price data in index table. + * + * @param int $productId + * @param array $expectedPrices + * @return void + */ + private function assertIndexTableData(int $productId, array $expectedPrices): void + { + $data = $this->getPriceIndexDataByProductId->execute( + $productId, + Group::NOT_LOGGED_IN_ID, + (int)$this->websiteRepository->get('base')->getId() + ); + $data = reset($data); + foreach ($expectedPrices as $column => $price) { + $this->assertEquals($price, $data[$column]); + } + } +} From 5e2e8e9fc9d47928ce66aea9dc502319dc5dd5b6 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Fri, 30 Oct 2020 19:38:51 +0200 Subject: [PATCH 130/195] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Magento/Cms/Test/Mftf/Data/BlockData.xml | 7 ++ ...dminUseQuickSearchInAdminDataGridsTest.xml | 109 ++++++++++++++++++ ...sertNumberOfRecordsInUiGridActionGroup.xml | 20 ++++ .../AdminGridHeadersSection.xml | 1 + 4 files changed, 137 insertions(+) create mode 100644 app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml create mode 100644 app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml index dea047ec43568..31dd27b0f599c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -15,4 +15,11 @@ <data key="content">sales25off everything!</data> <data key="is_active">0</data> </entity> + <entity name="TestBlock" type="block"> + <data key="title" unique="suffix">Test Block</data> + <data key="identifier" unique="suffix">TestBlock</data> + <data key="store_id">All Store Views</data> + <data key="content">Test Block content</data> + <data key="is_active">1</data> + </entity> </entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml new file mode 100644 index 0000000000000..85bbf6042b686 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -0,0 +1,109 @@ +<?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="AdminUseQuickSearchInAdminDataGridsTest"> + <annotations> + <features value="CmsPage"/> + <stories value="Create CMS Page"/> + <title value="[CMS Grids] Use quick search in Admin data grids"/> + <description value="Verify that Merchant can use quick search in order to simplify the data grid filtering in Admin"/> + <testCaseId value="MC-27559" /> + <severity value="MAJOR"/> + <group value="cms"/> + <group value="ui"/> + </annotations> + <before> + <createData entity="simpleCmsPage" stepKey="createFirstCMSPage" /> + <createData entity="_newDefaultCmsPage" stepKey="createSecondCMSPage" /> + <createData entity="_emptyCmsPage" stepKey="createThirdCMSPage" /> + <createData entity="Sales25offBlock" stepKey="createFirstCmsBlock"/> + <createData entity="TestBlock" stepKey="createSecondCmsBlock"/> + <createData entity="_emptyCmsBlock" stepKey="createThirdCmsBlock"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstCMSPage" stepKey="deleteFirstCMSPage" /> + <deleteData createDataKey="createSecondCMSPage" stepKey="deleteSecondCMSPage" /> + <deleteData createDataKey="createThirdCMSPage" stepKey="deleteThirdCMSPage" /> + <deleteData createDataKey="createFirstCmsBlock" stepKey="deleteFirstCmsBlock" /> + <deleteData createDataKey="createSecondCmsBlock" stepKey="deleteSecondCmsBlock" /> + <deleteData createDataKey="createThirdCmsBlock" stepKey="deleteThirdCmsBlock" /> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCMSPageGrid"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearCmsPagesGridFilters"/> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCmsBlockGrid"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearCmsBlockGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Go to "Cms Pages Grid" page and filter by title--> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstCmsPage"> + <argument name="keyword" value="$createFirstCMSPage.title$"/> + </actionGroup> + <see userInput="$createFirstCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeFirstCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsInCmsPageGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchSecondCmsPage"> + <argument name="keyword" value="$createSecondCMSPage.title$"/> + </actionGroup> + <see userInput="$createSecondCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeSecondCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringSecondCmsPage"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFilters"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsCmsPagesBeforeClickSearchButton"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSearchMagnifierButton"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsCmsPagesAfterClickSearchButton"/> + <assertEquals stepKey="assertTotalRecordsCmsPages"> + <expectedResult type="string">$grabTotalRecordsCmsPagesBeforeClickSearchButton</expectedResult> + <actualResult type="string">$grabTotalRecordsCmsPagesAfterClickSearchButton</actualResult> + </assertEquals> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="enterNonExistentEntityInQuickSearch"> + <argument name="keyword" value="TestQueryNonExistentEntity"/> + </actionGroup> + <dontSeeElement selector="{{AdminDataGridTableSection.rows}}" stepKey="dontSeeResultRows"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringNonExistentCmsPage"> + <argument name="number" value="0"/> + </actionGroup> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchThirdCmsPage"> + <argument name="keyword" value="$createThirdCMSPage.title$"/> + </actionGroup> + <see userInput="$createThirdCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeThirdCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringThirdCmsPage"/> + + <!--Go to "Cms Blocks Grid" page and filter by title--> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCmsBlockGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstCmsBlock"> + <argument name="keyword" value="$createFirstCmsBlock.title$"/> + </actionGroup> + <see userInput="$createFirstCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeFirstCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsInBlockGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchSecondCmsBlock"> + <argument name="keyword" value="$createSecondCmsBlock.title$"/> + </actionGroup> + <see userInput="$createSecondCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeSecondCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringSecondBlock"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersOnBlocksGridPage"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsBlocksBeforeClickSearchButton"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSearchMagnifierButtonOnBlocksGridPage"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsBlocksAfterClickSearchButton"/> + <assertEquals stepKey="assertTotalRecordsBlocks"> + <expectedResult type="string">$grabTotalRecordsBlocksBeforeClickSearchButton</expectedResult> + <actualResult type="string">$grabTotalRecordsBlocksAfterClickSearchButton</actualResult> + </assertEquals> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="enterNonExistentEntityInQuickSearchOnBlocksGridPage"> + <argument name="keyword" value="TestQueryNonExistentEntity"/> + </actionGroup> + <dontSeeElement selector="{{AdminDataGridTableSection.rows}}" stepKey="dontSeeResultRowsOnBlocksGrid"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringNonExistentCmsBlock"> + <argument name="number" value="0"/> + </actionGroup> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchThirdCmsBlock"> + <argument name="keyword" value="$createThirdCmsBlock.title$"/> + </actionGroup> + <see userInput="$createThirdCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeThirdCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringThirdBlock"/> + </test> +</tests> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml new file mode 100644 index 0000000000000..5928833bf4794 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertNumberOfRecordsInUiGridActionGroup"> + <annotations> + <description>Validates that the Number of Records listed on the Ui grid page is present and correct.</description> + </annotations> + <arguments> + <argument name="number" type="string" defaultValue="1"/> + </arguments> + <see userInput="{{number}} records found" selector="{{AdminGridHeaders.totalRecords}}" stepKey="seeRecords"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml index 89831359657bf..851e65e61bb1c 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml @@ -11,5 +11,6 @@ <element name="title" type="text" selector=".page-title-wrapper h1"/> <element name="headerByName" type="text" selector="//div[@data-role='grid-wrapper']//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> <element name="columnsNames" type="text" selector="[data-role='grid-wrapper'] .data-grid-th > span"/> + <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row .row>div:nth-child(1)"/> </section> </sections> From 3464b87a6954dd939bf8b97b2a10022bc8cfb425 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Mon, 2 Nov 2020 11:55:30 +0200 Subject: [PATCH 131/195] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml index 85bbf6042b686..65df6fd80e865 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminUseQuickSearchInAdminDataGridsTest"> <annotations> - <features value="CmsPage"/> + <features value="Cms"/> <stories value="Create CMS Page"/> <title value="[CMS Grids] Use quick search in Admin data grids"/> <description value="Verify that Merchant can use quick search in order to simplify the data grid filtering in Admin"/> From 362eba826be3a7aff9ce9de579893e639ffc2cfd Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <duhon@users.noreply.github.com> Date: Mon, 2 Nov 2020 11:03:20 -0600 Subject: [PATCH 132/195] [performance] MC-37936: Performance generator for images (#6295) --- app/code/Magento/MediaStorage/App/Media.php | 2 +- app/code/Magento/Swatches/etc/config.xml | 7 +++++++ setup/src/Magento/Setup/Fixtures/ImagesFixture.php | 13 +++++++++++-- .../Fixtures/ImagesGenerator/ImagesGenerator.php | 13 +++++++++---- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaStorage/App/Media.php b/app/code/Magento/MediaStorage/App/Media.php index f3a85cf3a9baa..34c20aab40bcb 100644 --- a/app/code/Magento/MediaStorage/App/Media.php +++ b/app/code/Magento/MediaStorage/App/Media.php @@ -255,7 +255,7 @@ private function setPlaceholderImage(): void */ private function getOriginalImage(string $resizedImagePath): string { - return preg_replace('|^.*((?:/[^/]+){3})$|', '$1', $resizedImagePath); + return preg_replace('|^.*?((?:/([^/])/([^/])/\2\3)?/?[^/]+$)|', '$1', $resizedImagePath); } /** diff --git a/app/code/Magento/Swatches/etc/config.xml b/app/code/Magento/Swatches/etc/config.xml index 9d36d9692b295..236e9237fb29b 100644 --- a/app/code/Magento/Swatches/etc/config.xml +++ b/app/code/Magento/Swatches/etc/config.xml @@ -14,6 +14,13 @@ <show_swatch_tooltip>1</show_swatch_tooltip> </frontend> </catalog> + <system> + <media_storage_configuration> + <allowed_resources> + <swatches_folder>attribute</swatches_folder> + </allowed_resources> + </media_storage_configuration> + </system> <general> <validator_data> <input_types> diff --git a/setup/src/Magento/Setup/Fixtures/ImagesFixture.php b/setup/src/Magento/Setup/Fixtures/ImagesFixture.php index 1878a48977156..cd403897de07a 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesFixture.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesFixture.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\ValidatorException; +use Magento\MediaStorage\Service\ImageResize; use Symfony\Component\Console\Output\OutputInterface; /** @@ -106,6 +107,10 @@ class ImagesFixture extends Fixture * @var array */ private $tableCache = []; + /** + * @var ImageResize + */ + private $imageResize; /** * @param FixtureModel $fixtureModel @@ -117,6 +122,7 @@ class ImagesFixture extends Fixture * @param \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory * @param \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param ImageResize $imageResize */ public function __construct( FixtureModel $fixtureModel, @@ -127,7 +133,8 @@ public function __construct( \Magento\Eav\Model\AttributeRepository $attributeRepository, \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory, \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + ImageResize $imageResize ) { parent::__construct($fixtureModel); @@ -139,6 +146,7 @@ public function __construct( $this->expressionFactory = $expressionFactory; $this->batchInsertFactory = $batchInsertFactory; $this->metadataPool = $metadataPool; + $this->imageResize = $imageResize; } /** @@ -147,9 +155,10 @@ public function __construct( */ public function execute() { - if (!$this->checkIfImagesExists()) { + if (!$this->checkIfImagesExists() && $this->getImagesToGenerate()) { $this->createImageEntities(); $this->assignImagesToProducts(); + iterator_to_array($this->imageResize->resizeFromThemes(), false); } } diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index 9b42548c4e105..bc6d57a869b5a 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -36,9 +36,9 @@ public function __construct( /** * Generates image from $data and puts its to /tmp folder - * * @param array $config * @return string $imagePath + * @throws \Exception */ public function generate($config) { @@ -70,9 +70,14 @@ public function generate($config) $relativePathToMedia = $mediaDirectory->getRelativePath($this->mediaConfig->getBaseTmpMediaPath()); $mediaDirectory->create($relativePathToMedia); - $absolutePathToMedia = $mediaDirectory->getAbsolutePath($this->mediaConfig->getBaseTmpMediaPath()); - $imagePath = $absolutePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; - imagejpeg($image, $imagePath, 100); + $imagePath = $relativePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; + $memory = fopen('php://memory', 'r+'); + if(!imagejpeg($image, $memory)) { + throw new \Exception('Could not create picture ' . $imagePath); + } + $mediaDirectory->writeFile($imagePath, stream_get_contents($memory, -1, 0)); + fclose($memory); + imagedestroy($image); // phpcs:enable return $imagePath; From c6627449678753ab930879d9e6faf3af6ed14d27 Mon Sep 17 00:00:00 2001 From: Dmytro Poperechnyy <dpoperechnyy@magento.com> Date: Mon, 2 Nov 2020 20:16:09 +0200 Subject: [PATCH 133/195] MC-37822: Support by TaxImportExport (#6269) --- .../TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php | 5 ++++- .../Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php index 844cfc535cfb2..f23fe8ffae7ae 100644 --- a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php +++ b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php @@ -81,7 +81,10 @@ public function execute() $content .= $rate->toString($template) . "\n"; } - return $this->fileFactory->create('tax_rates.csv', $content, DirectoryList::VAR_DIR); + // pass 'rm' parameter to delete a file after download + $fileContent = ['type' => 'string', 'value' => $content, 'rm' => true]; + + return $this->fileFactory->create('tax_rates.csv', $fileContent, DirectoryList::VAR_DIR); } /** diff --git a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php index 0c8d0cf80544b..f4d31f3e421eb 100644 --- a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php +++ b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php @@ -101,10 +101,11 @@ public function testExecute() ]); $rateCollectionMock->expects($this->once())->method('joinCountryTable')->willReturnSelf(); $rateCollectionMock->expects($this->once())->method('joinRegionTable')->willReturnSelf(); + $fileContent = ['type' => 'string', 'value' => $content, 'rm' => true]; $this->fileFactoryMock ->expects($this->once()) ->method('create') - ->with('tax_rates.csv', $content, DirectoryList::VAR_DIR); + ->with('tax_rates.csv', $fileContent, DirectoryList::VAR_DIR); $this->controller->execute(); } } From 672768c1061940d320ad62ab0c84847235054aff Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 3 Nov 2020 13:31:19 +0200 Subject: [PATCH 134/195] MC-38851:[MFTF] AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest failed because of bad design --- ...alogUiVerifyUsedInLinkCategoryGridTest.xml | 7 +- ...ogUiVerifyUsedInLinkedCategoryGridTest.xml | 102 ++++++++++++++++++ ...ediaGalleryImageDetailsEditActionGroup.xml | 2 +- ...ediaGalleryImageDetailsSaveActionGroup.xml | 2 +- ...EnhancedMediaGalleryEditDetailsSection.xml | 3 +- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index f9ffda43d2547..a3f1bd7c01136 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest"> + <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest" deprecated="Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead"> <annotations> <features value="AdminMediaGalleryCategoryGrid"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> - <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <title value="DEPRECATED. User can open each entity the asset is associated with in a separate tab to manage association"/> <stories value="Story 58: User sees entities where asset is used in" /> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml new file mode 100644 index 0000000000000..8e197b740bb11 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml @@ -0,0 +1,102 @@ +<?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="AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + </before> + + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="openCategoryPage"> + <argument name="id" value="$category.id$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <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="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> + + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderToVerifyLink"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCategoryImageFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInCategories"> + <argument name="entityName" value="Categories"/> + </actionGroup> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"> + <argument name="file" value="{{UpdatedImageDetails.file}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryInGrid"> + <argument name="category" value="$category$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetCategoriesGridFilters"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setAssetFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterAppliedAfterUrlFilterApplier"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="openCategoryImageFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml index 931da0ee06fef..5f7ab2d2d008f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml @@ -13,6 +13,6 @@ <description>Edit image from the View Details panel</description> </annotations> <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryEditDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml index 0da3de9501c13..69e8d94522cde 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -19,6 +19,6 @@ <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.title}}" userInput="{{image.title}}" stepKey="setTitle" /> <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.description}}" userInput="{{image.description}}" stepKey="setDescription" /> <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.save}}" stepKey="saveDetails"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml index b0bed4563003e..351367055e62b 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -16,6 +16,7 @@ <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> <element name="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{keyword}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> <element name="cancel" type="button" selector="#image-details-action-cancel"/> - <element name="save" type="button" selector="#image-details-action-save"/> + <element name="save" type="button" selector="#image-details-action-save" timeout="30"/> + <element name="modalTitle" type="text" selector="//aside[contains(@class, 'media-gallery-edit-image-details') and contains(@class, '_show')]//h1[contains(., 'Edit Image')]"/> </section> </sections> From 0f9ac99527af2fcde10956668d1789f50abb4311 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 3 Nov 2020 13:40:53 +0200 Subject: [PATCH 135/195] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../Customer/Address/Billing/Address.php | 54 +++++++++++++++++++ ...rder_create_load_block_billing_address.xml | 1 + .../templates/order/create/form/address.phtml | 9 +++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php diff --git a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php new file mode 100644 index 0000000000000..a7ec8e6587d70 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\ViewModel\Customer\Address\Billing; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Sales\Model\AdminOrder\Create; +use Magento\Quote\Model\Quote\Address as QuoteAddress; + +/** + * Customer address formatter + */ +class Address implements ArgumentInterface +{ + /** + * @var Create + */ + protected $orderCreate; + + /** + * Customer billing address + * + * @param Create $orderCreate + */ + public function __construct( + Create $orderCreate + ) { + $this->orderCreate = $orderCreate; + } + + /** + * Return billing address object + * + * @return QuoteAddress + */ + public function getAddress(): QuoteAddress + { + return $this->orderCreate->getBillingAddress(); + } + + /** + * Get save billing address in the address book + * + * @return int + */ + public function getSaveInAddressBook(): int + { + return (int)$this->getAddress()->getSaveInAddressBook(); + } +} diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index c52f81d5cb56d..edefa8de55c7a 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -12,6 +12,7 @@ <arguments> <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + <argument name="customerBillingAddress" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\Address</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 12927dcf526a3..bdb1a6c8cba94 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -24,6 +24,11 @@ endif; */ $customerAddressFormatter = $block->getData('customerAddressFormatter'); +/** + * @var \Magento\Sales\ViewModel\Customer\Address\Billing\Address $billingAddress + */ +$billingAddress = $block->getData('customerBillingAddress'); + /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| * \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block @@ -114,7 +119,9 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if (!$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + <?php if ($billingAddress && $billingAddress->getSaveInAddressBook()): ?> + checked="checked" + <?php elseif ($block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> From 88051820ac133b5921f85f7b18ddbebd14d5b0e5 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 3 Nov 2020 13:52:05 +0200 Subject: [PATCH 136/195] MC-38682: Can't create shipping label for existing order with FedEx shipping --- .../adminhtml/templates/order/packaging/popup_content.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index c3418049a38a0..71299b33ff159 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -227,8 +227,8 @@ <div class="grid_prepare admin__page-subsection"></div> </div> </section> - <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <div id="packages_content"></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <?php $scriptString = <<<script require(['jquery'], function($){ $("div#packages_content").on('click', "button[data-action='package-save-items']", From 9fb6e86600e53f6c46692e4db5ae91f0633e9a13 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Tue, 3 Nov 2020 13:59:02 +0200 Subject: [PATCH 137/195] MC-37816: Performance - endless scheduled export of catalog with 100k+ products --- .../DownloadableImportExport/Model/Export/RowCustomizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php index daa874e829e54..3b805fef73434 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php @@ -82,7 +82,7 @@ public function prepareData($collection, $productIds): void ->addAttributeToSelect('samples_title'); // set global scope during export $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); - foreach ($collection as $product) { + foreach ($productCollection as $product) { $productLinks = $this->linkRepository->getLinksByProduct($product); $productSamples = $this->sampleRepository->getSamplesByProduct($product); $this->downloadableData[$product->getId()] = []; From afab3b5843b66b966f05d5f516e8e8a71049c643 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Tue, 3 Nov 2020 14:40:29 +0200 Subject: [PATCH 138/195] MC-37542: Create automated test for "[API] Create CMS page using API service" --- .../Magento/Cms/Api/PageRepositoryTest.php | 449 +++++++++++++----- 1 file changed, 341 insertions(+), 108 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index 757530c4da693..773a5d3fd8596 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -11,15 +11,19 @@ use Magento\Authorization\Model\RulesFactory; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\Data\PageInterfaceFactory; +use Magento\Cms\Ui\Component\DataProvider as CmsDataProvider; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\AdminTokenServiceInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -83,18 +87,55 @@ class PageRepositoryTest extends WebapiAbstract */ private $createdPages = []; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var CmsDataProvider + */ + private $cmsUiDataProvider; + + /** + * @var GetPageByIdentifierInterface + */ + private $getPageByIdentifier; + /** * @inheritdoc */ protected function setUp(): void { - $this->pageFactory = Bootstrap::getObjectManager()->create(PageInterfaceFactory::class); - $this->pageRepository = Bootstrap::getObjectManager()->create(PageRepositoryInterface::class); - $this->dataObjectHelper = Bootstrap::getObjectManager()->create(DataObjectHelper::class); - $this->dataObjectProcessor = Bootstrap::getObjectManager()->create(DataObjectProcessor::class); - $this->roleFactory = Bootstrap::getObjectManager()->get(RoleFactory::class); - $this->rulesFactory = Bootstrap::getObjectManager()->get(RulesFactory::class); - $this->adminTokens = Bootstrap::getObjectManager()->get(AdminTokenServiceInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->pageFactory = $this->objectManager->create(PageInterfaceFactory::class); + $this->pageRepository = $this->objectManager->create(PageRepositoryInterface::class); + $this->dataObjectHelper = $this->objectManager->create(DataObjectHelper::class); + $this->dataObjectProcessor = $this->objectManager->create(DataObjectProcessor::class); + $this->roleFactory = $this->objectManager->get(RoleFactory::class); + $this->rulesFactory = $this->objectManager->get(RulesFactory::class); + $this->adminTokens = $this->objectManager->get(AdminTokenServiceInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->filterBuilder = $this->objectManager->get(FilterBuilder::class); + $this->cmsUiDataProvider = $this->objectManager->create( + CmsDataProvider::class, + [ + 'name' => 'cms_page_listing_data_source', + 'primaryFieldName' => 'page_id', + 'requestFieldName' => 'id', + ] + ); + $this->getPageByIdentifier = $this->objectManager->get(GetPageByIdentifierInterface::class); } /** @@ -127,17 +168,11 @@ public function testGet(): void ->setIdentifier($pageIdentifier); $this->currentPage = $this->pageRepository->save($pageDataObject); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $this->currentPage->getId(), - 'httpMethod' => Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'GetById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'GetById', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . '/' . $this->currentPage->getId() + ); $page = $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $this->currentPage->getId()]); $this->assertNotNull($page['id']); @@ -147,6 +182,41 @@ public function testGet(): void $this->assertEquals($pageData->getIdentifier(), $pageIdentifier); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testGetByStores(string $requestStore): void + { + $page = $this->getPageByIdentifier->execute('page100', 0); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->updatePage($page, ['store_id' => $store->getId()]); + $page = $this->getPageByIdentifier->execute('page100', $store->getId()); + $comparedFields = $this->getPageRequestData()['page']; + $expectedData = array_intersect_key( + $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), + $comparedFields + ); + $serviceInfo = $this->getServiceInfo( + 'GetById', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = []; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData[PageInterface::PAGE_ID] = $page->getId(); + } + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertNotNull($page['id']); + $actualData = array_intersect_key($page, $comparedFields); + $this->assertEquals($expectedData, $actualData, 'Error while getting page.'); + } + /** * Test create page * @@ -161,17 +231,7 @@ public function testCreate(): void $pageDataObject->setTitle($pageTitle) ->setIdentifier($pageIdentifier); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ 'page' => [ @@ -187,10 +247,44 @@ public function testCreate(): void $this->assertEquals($this->currentPage->getIdentifier(), $pageIdentifier); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testCreateByStores(string $requestStore): void + { + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); + $requestData = $this->getPageRequestData(); + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertNotNull($page['id']); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->currentPage = $this->getPageByIdentifier->execute( + $requestData['page'][PageInterface::IDENTIFIER], + $store->getId() + ); + $actualData = array_intersect_key($page, $requestData['page']); + $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); + if ($requestStore != 'all') { + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + $pageGridData = $this->cmsUiDataProvider->getData(); + $this->assertTrue( + $this->isPageInArray($pageGridData['items'], $page['id']), + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + ); + } + /** * Test update \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testUpdate() + public function testUpdate(): void { $pageTitle = self::PAGE_TITLE; $newPageTitle = self::PAGE_TITLE_NEW; @@ -210,17 +304,10 @@ public function testUpdate() PageInterface::class ); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_POST + ); $page = $this->_webApiCall($serviceInfo, ['page' => $pageData]); $this->assertNotNull($page['id']); @@ -249,17 +336,11 @@ public function testUpdateOneField(): void $this->currentPage = $this->pageRepository->save($pageDataObject); $pageId = $this->currentPage->getId(); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $pageId, - 'httpMethod' => Request::HTTP_METHOD_PUT, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_PUT, + self::RESOURCE_PATH . '/' . $pageId + ); $data = [ 'page' => [ @@ -283,12 +364,54 @@ public function testUpdateOneField(): void $this->assertEquals($page['content'], $content); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testUpdateByStores(string $requestStore): void + { + $page = $this->getPageByIdentifier->execute('page100', 0); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->updatePage($page, ['store_id' => $store->getId()]); + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_PUT, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = $this->getPageRequestData(); + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertNotNull($page['id']); + $this->currentPage = $this->getPageByIdentifier->execute( + $requestData['page'][PageInterface::IDENTIFIER], + $store->getId() + ); + $actualData = array_intersect_key($page, $requestData['page']); + $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); + if ($requestStore != 'all') { + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + $pageGridData = $this->cmsUiDataProvider->getData(); + $this->assertTrue( + $this->isPageInArray($pageGridData['items'], $page['id']), + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + ); + } + /** * Test delete \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testDelete() + public function testDelete(): void { - $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + $this->expectException(NoSuchEntityException::class); $pageTitle = self::PAGE_TITLE; $pageIdentifier = self::PAGE_IDENTIFIER_PREFIX . uniqid(); @@ -298,34 +421,66 @@ public function testDelete() ->setIdentifier($pageIdentifier); $this->currentPage = $this->pageRepository->save($pageDataObject); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $this->currentPage->getId(), - 'httpMethod' => Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'DeleteById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $this->currentPage->getId() + ); $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $this->currentPage->getId()]); $this->pageRepository->getById($this->currentPage['id']); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testDeleteByStores(string $requestStore): void + { + $page = $this->getPageByIdentifier->execute('page100', 0); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->updatePage($page, ['store_id' => $store->getId()]); + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = []; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData[PageInterface::PAGE_ID] = $page->getId(); + } + $pageResponse = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertTrue($pageResponse); + if ($requestStore != 'all') { + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + $pageGridData = $this->cmsUiDataProvider->getData(); + $this->assertFalse( + $this->isPageInArray($pageGridData['items'], $page->getId()), + sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $storeCode) + ); + } + /** * Test search \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testSearch() + public function testSearch(): void { $cmsPages = $this->prepareCmsPages(); /** @var FilterBuilder $filterBuilder */ - $filterBuilder = Bootstrap::getObjectManager()->create(FilterBuilder::class); + $filterBuilder = $this->objectManager->create(FilterBuilder::class); /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager() + $searchCriteriaBuilder = $this->objectManager ->create(SearchCriteriaBuilder::class); $filter1 = $filterBuilder @@ -351,7 +506,7 @@ public function testSearch() $searchCriteriaBuilder->addFilters([$filter3, $filter4]); /** @var SortOrderBuilder $sortOrderBuilder */ - $sortOrderBuilder = Bootstrap::getObjectManager()->create(SortOrderBuilder::class); + $sortOrderBuilder = $this->objectManager->create(SortOrderBuilder::class); /** @var SortOrder $sortOrder */ $sortOrder = $sortOrderBuilder->setField(PageInterface::IDENTIFIER) @@ -365,17 +520,11 @@ public function testSearch() $searchData = $searchCriteriaBuilder->create()->__toArray(); $requestData = ['searchCriteria' => $searchData]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . "/search" . '?' . http_build_query($requestData), - 'httpMethod' => Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'GetList', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'GetList', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . "/search" . '?' . http_build_query($requestData) + ); $searchResult = $this->_webApiCall($serviceInfo, $requestData); $this->assertEquals(2, $searchResult['total_count']); @@ -388,8 +537,10 @@ public function testSearch() /** * Create page with the same identifier after one was removed. + * + * @return void */ - public function testCreateSamePage() + public function testCreateSamePage(): void { $pageIdentifier = self::PAGE_IDENTIFIER_PREFIX . uniqid(); @@ -399,10 +550,30 @@ public function testCreateSamePage() $this->currentPage = $this->pageRepository->getById($id); } + /** + * Get stores for CRUD operations + * + * @return array + */ + public function byStoresProvider(): array + { + return [ + 'default_store' => [ + 'request_store' => 'default', + ], + /*'second_store' => [ + 'request_store' => 'fixture_second_store', + ], + 'all' => [ + 'request_store' => 'all', + ],*/ + ]; + } + /** * @return PageInterface[] */ - private function prepareCmsPages() + private function prepareCmsPages(): array { $result = []; @@ -437,19 +608,9 @@ private function prepareCmsPages() * @param string $identifier * @return string */ - private function createPageWithIdentifier($identifier) + private function createPageWithIdentifier($identifier): string { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ 'page' => [ PageInterface::IDENTIFIER => $identifier, @@ -466,19 +627,13 @@ private function createPageWithIdentifier($identifier) * @param string $pageId * @return void */ - private function deletePageByIdentifier($pageId) + private function deletePageByIdentifier($pageId): void { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $pageId, - 'httpMethod' => Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'DeleteById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $pageId + ); $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $pageId]); } @@ -547,7 +702,7 @@ public function testSaveDesign(): void //Updating the user role to allow access to design properties. /** @var Rules $rules */ - $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules = $this->objectManager->create(Rules::class); $rules->setRoleId($role->getId()); $rules->setResources(['Magento_Cms::page', 'Magento_Cms::save_design']); $rules->saveRel(); @@ -562,7 +717,7 @@ public function testSaveDesign(): void //Updating our role to remove design properties access. /** @var Rules $rules */ - $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules = $this->objectManager->create(Rules::class); $rules->setRoleId($role->getId()); $rules->setResources(['Magento_Cms::page']); $rules->saveRel(); @@ -587,4 +742,82 @@ public function testSaveDesign(): void //We don't have permissions to do that. $this->assertEquals('You are not allowed to change CMS pages design settings', $exceptionMessage); } + + /** + * Get service info array + * + * @param string $soapOperation + * @param string $httpMethod + * @param string $resourcePath + * @return array + */ + private function getServiceInfo( + string $soapOperation, + string $httpMethod, + string $resourcePath = self::RESOURCE_PATH + ): array { + return [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => $httpMethod, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . $soapOperation, + ], + ]; + } + + /** + * Check that the page is in the page grid data + * + * @param array $pageGridData + * @param int $pageId + * @return bool + */ + private function isPageInArray(array $pageGridData, int $pageId): bool + { + $isPagePresent = false; + foreach ($pageGridData as $pageData) { + if ($pageData['page_id'] == $pageId) { + $isPagePresent = true; + break; + } + } + + return $isPagePresent; + } + + /** + * Update page with data + * + * @param PageInterface $page + * @param array $pageData + * @return PageInterface + */ + private function updatePage(PageInterface $page, array $pageData): PageInterface + { + $page->addData($pageData); + + return $this->pageRepository->save($page); + } + + /** + * Get request data for create or update page + * + * @return array + */ + private function getPageRequestData(): array + { + return [ + 'page' => [ + PageInterface::IDENTIFIER => self::PAGE_IDENTIFIER_PREFIX . uniqid(), + PageInterface::TITLE => self::PAGE_TITLE . uniqid(), + 'active' => true, + PageInterface::PAGE_LAYOUT => '1column', + PageInterface::CONTENT => self::PAGE_CONTENT, + ] + ]; + } } From c4953900027ae2523c15e35d7ab5c6e7fc5c2954 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Tue, 3 Nov 2020 14:57:25 +0200 Subject: [PATCH 139/195] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml | 4 ++-- .../Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml | 2 +- .../AdminGridControlsSection/AdminGridHeadersSection.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml index 31dd27b0f599c..bf9f199634078 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -15,9 +15,9 @@ <data key="content">sales25off everything!</data> <data key="is_active">0</data> </entity> - <entity name="TestBlock" type="block"> + <entity name="ActiveTestBlock" type="block"> <data key="title" unique="suffix">Test Block</data> - <data key="identifier" unique="suffix">TestBlock</data> + <data key="identifier" unique="suffix">ActiveTestBlock</data> <data key="store_id">All Store Views</data> <data key="content">Test Block content</data> <data key="is_active">1</data> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml index 65df6fd80e865..245b1486058b8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -23,7 +23,7 @@ <createData entity="_newDefaultCmsPage" stepKey="createSecondCMSPage" /> <createData entity="_emptyCmsPage" stepKey="createThirdCMSPage" /> <createData entity="Sales25offBlock" stepKey="createFirstCmsBlock"/> - <createData entity="TestBlock" stepKey="createSecondCmsBlock"/> + <createData entity="ActiveTestBlock" stepKey="createSecondCmsBlock"/> <createData entity="_emptyCmsBlock" stepKey="createThirdCmsBlock"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml index 851e65e61bb1c..c7aa7604d7ade 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml @@ -11,6 +11,6 @@ <element name="title" type="text" selector=".page-title-wrapper h1"/> <element name="headerByName" type="text" selector="//div[@data-role='grid-wrapper']//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> <element name="columnsNames" type="text" selector="[data-role='grid-wrapper'] .data-grid-th > span"/> - <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row .row>div:nth-child(1)"/> + <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row.row.row-gutter div.row div.admin__control-support-text"/> </section> </sections> From 1886177acf3e36a2f3c8c666cad53555d3a17eb3 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 3 Nov 2020 17:22:21 +0200 Subject: [PATCH 140/195] MC-37902: Create automated test for "Paging and sort by function on widget grid" --- .../Block/Adminhtml/Widget/InstanceTest.php | 308 ++++++++++++++++++ .../Magento/Widget/_files/widgets.php | 103 ++++++ .../Widget/_files/widgets_rollback.php | 25 ++ 3 files changed, 436 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php create mode 100644 dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php new file mode 100644 index 0000000000000..3e01305d9f39f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php @@ -0,0 +1,308 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Widget\Block\Adminhtml\Widget; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme as ThemeResource; +use Magento\Theme\Model\ThemeFactory; +use PHPUnit\Framework\TestCase; + +/** + * Checks widget grid filtering and sorting + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class InstanceTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var PageFactory */ + private $pageFactory; + + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + } + + /** + * @dataProvider gridFiltersDataProvider + * + * @magentoDataFixture Magento/Widget/_files/widgets.php + * + * @param array $filter + * @param array $expectedWidgets + * @return void + */ + public function testGridFiltering(array $filter, array $expectedWidgets): void + { + $this->request->setParams($filter); + $collection = $this->getGridCollection(); + + $this->assertWidgets($expectedWidgets, $collection); + } + + /** + * @return array + */ + public function gridFiltersDataProvider(): array + { + return [ + 'first_page' => [ + 'filter' => [ + 'limit' => 2, + 'page' => 1, + ], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + ], + ], + 'second_page' => [ + 'filter' => [ + 'limit' => 2, + 'page' => 2, + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + 'filter_by_title' => [ + 'filter' => [ + 'filter' => base64_encode('title=product link widget title'), + ], + 'expected_widgets' => [ + 'product link widget title', + ], + ], + 'filter_by_type' => [ + 'filter' => [ + 'filter' => base64_encode('type=Magento%5CCms%5CBlock%5CWidget%5CPage%5CLink'), + ], + 'expected_widgets' => [ + 'cms page widget title', + ], + ], + 'filter_by_theme' => [ + 'filter' => [ + 'filter' => base64_encode('theme_id=' . $this->loadThemeIdByCode('Magento/blank')), + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + 'filter_by_sort_order' => [ + 'filter' => [ + 'filter' => base64_encode('sort_order=1'), + ], + 'expected_widgets' => [ + 'recently compared products' + ], + ], + 'filter_by_multiple_filters' => [ + 'filter' => [ + 'filter' => base64_encode( + 'type=Magento%5CCatalog%5CBlock%5CWidget%5CRecentlyCompared&sort_order=1' + ), + ], + 'expected_widgets' => [ + 'recently compared products' + ], + ], + ]; + } + + /** + * @dataProvider gridSortDataProvider + * + * @magentoDataFixture Magento/Widget/_files/widgets.php + * + * @param array $filter + * @param array $expectedWidgets + * @return void + */ + public function testGridSorting(array $filter, array $expectedWidgets): void + { + $this->request->setParams($filter); + $collection = $this->getGridCollection(); + $this->assertCount(count($expectedWidgets), $collection); + $this->assertEquals($expectedWidgets, $collection->getColumnValues('title')); + } + + /** + * @return array + */ + public function gridSortDataProvider(): array + { + return [ + 'sort_by_id_asc' => [ + 'filter' => ['sort' => 'instance_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_id_desc' => [ + 'filter' => ['sort' => 'instance_id', 'dir' => 'desc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_title_asc' => [ + 'filter' => ['sort' => 'title', 'dir' => 'asc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_title_desc' => [ + 'filter' => ['sort' => 'title', 'dir' => 'desc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_type_asc' => [ + 'filter' => ['sort' => 'type', 'dir' => 'asc'], + 'expected_widgets' => [ + 'product link widget title', + 'recently compared products', + 'cms page widget title', + ], + ], + 'sort_by_type_desc' => [ + 'filter' => ['sort' => 'type', 'dir' => 'desc'], + 'expected_widgets' => [ + 'cms page widget title', + 'recently compared products', + 'product link widget title', + ], + ], + 'sort_by_sort_order_asc' => [ + 'filter' => ['sort' => 'sort_order', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_sort_order_desc' => [ + 'filter' => ['sort' => 'sort_order', 'dir' => 'desc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_theme_asc' => [ + 'filter' => ['sort' => 'theme_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'cms page widget title', + 'product link widget title', + ], + ], + 'sort_by_theme_desc' => [ + 'filter' => ['sort' => 'theme_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'cms page widget title', + 'product link widget title', + ], + ], + ]; + } + + /** + * Load theme by theme id + * + * @param string $code + * @return int + */ + private function loadThemeIdByCode(string $code): int + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ThemeFactory $themeFactory */ + $themeFactory = $objectManager->get(ThemeFactory::class); + /** @var ThemeResource $themeResource */ + $themeResource = $objectManager->get(ThemeResource::class); + $theme = $themeFactory->create(); + $themeResource->load($theme, $code, 'code'); + + return (int)$theme->getId(); + } + + /** + * Assert widget instances + * + * @param $expectedWidgets + * @param AbstractCollection $collection + * @return void + */ + private function assertWidgets($expectedWidgets, AbstractCollection $collection): void + { + $this->assertCount(count($expectedWidgets), $collection); + foreach ($expectedWidgets as $widgetTitle) { + $item = $collection->getItemByColumnValue('title', $widgetTitle); + $this->assertNotNull($item); + } + } + + /** + * Prepare page layout + * + * @return LayoutInterface + */ + private function preparePageLayout(): LayoutInterface + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'adminhtml_widget_instance_index', + ]); + + return $page->getLayout()->generateXml(); + } + + /** + * Get prepared grid collection + * + * @return AbstractCollection + */ + private function getGridCollection(): AbstractCollection + { + $layout = $this->preparePageLayout(); + $containerBlock = $layout->getBlock('adminhtml.widget.instance.grid.container'); + $grid = $containerBlock->getChildBlock('grid'); + $this->assertNotFalse($grid); + + return $grid->getPreparedCollection(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php new file mode 100644 index 0000000000000..fa0f7d9fe9918 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Block\Product\Widget\Link as ProductLink; +use Magento\Catalog\Block\Widget\RecentlyCompared; +use Magento\Cms\Api\GetPageByIdentifierInterface; +use Magento\Cms\Block\Widget\Page\Link as PageLink; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme as ThemeResource; +use Magento\Theme\Model\ThemeFactory; +use Magento\Widget\Model\ResourceModel\Widget\Instance as InstanceResource; +use Magento\Widget\Model\Widget\InstanceFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ThemeFactory $themeFactory */ +$themeFactory = $objectManager->get(ThemeFactory::class); +/** @var ThemeResource $themeResource */ +$themeResource = $objectManager->get(ThemeResource::class); +$lumaTheme = $themeFactory->create(); +$themeResource->load($lumaTheme, 'Magento/luma', 'code'); +$blankTheme = $themeFactory->create(); +$themeResource->load($blankTheme, 'Magento/blank', 'code'); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultStoreId = (int)$storeManager->getStore('default')->getId(); +/** @var GetPageByIdentifierInterface $getPageByIdentifier */ +$getPageByIdentifier = $objectManager->get(GetPageByIdentifierInterface::class); +$homePage = $getPageByIdentifier->execute('home', $defaultStoreId); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$productId = (int)$productRepository->get('simple2')->getId(); +/** @var InstanceFactory $widgetFactory */ +$widgetFactory = $objectManager->get(InstanceFactory::class); +/** @var InstanceResource $widgetResource */ +$widgetResource = $objectManager->get(InstanceResource::class); +$cmsPageWidget = $widgetFactory->create(); +$cmsPageWidgetData = [ + 'instance_type' => PageLink::class, + 'instance_code' => 'cms_page_link', + 'theme_id' => $lumaTheme->getId(), + 'title' => 'cms page widget title', + 'sort_order' => 3, + 'store_ids' => [$defaultStoreId], + 'widget_parameters' => [ + 'page_id' => $homePage->getId(), + ], +]; +$cmsPageWidget->setData($cmsPageWidgetData); +$widgetResource->save($cmsPageWidget); + +$productLinkWidget = $widgetFactory->create(); +$productLinkWidgetData = [ + 'instance_type' => ProductLink::class, + 'instance_code' => 'catalog_product_link', + 'theme_id' => $lumaTheme->getId(), + 'title' => 'product link widget title', + 'sort_order' => 2, + 'store_ids' => [$defaultStoreId], + 'pages_groups' => [ + 'page_group' => 'all_pages', + 'all_pages' => [ + 'page_id' => 0, + 'layout_handle' => 'default', + 'for' => 'all', + 'block' => 'content', + 'template' => 'product/widget/link/link_block.phtml', + ], + ], + 'widget_parameters' => [ + 'product/' . $productId, + ], +]; + +$productLinkWidget->setData($productLinkWidgetData); +$widgetResource->save($productLinkWidget); + +$recentlyComparedProductWidget = $widgetFactory->create(); +$recentlyComparedProductWidgetData = [ + 'instance_type' => RecentlyCompared::class, + 'instance_code' => 'catalog_recently_compared', + 'theme_id' => $blankTheme->getId(), + 'title' => 'recently compared products', + 'store_ids' => [$defaultStoreId], + 'sort_order' => 1, + 'widget_parameters' => [ + 'uiComponent' => 'widget_recently_compared', + 'page_size' => 5, + 'show_attributes' => ['name'], + 'show_buttons' => ['add_to_cart'], + ], +]; +$recentlyComparedProductWidget->setData($recentlyComparedProductWidgetData); +$widgetResource->save($recentlyComparedProductWidget); diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php new file mode 100644 index 0000000000000..63c8183fe3431 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Widget\Model\ResourceModel\Widget\Instance; +use Magento\Widget\Model\ResourceModel\Widget\Instance\CollectionFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CollectionFactory $collectionFactory */ +$collectionFactory = $objectManager->get(CollectionFactory::class); +/** @var Instance $widgetResourceModel */ +$widgetResourceModel = $objectManager->get(Instance::class); + +$titles = ['cms page widget title', 'product link widget title', 'recently compared products']; +$widgets = $collectionFactory->create()->addFieldToFilter('title', $titles); +foreach ($widgets as $widget) { + $widgetResourceModel->delete($widget); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); From 987f4b8bf5c628eb4474ac553d74792d8fd6f2ab Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 3 Nov 2020 18:27:23 +0200 Subject: [PATCH 141/195] MC-38851:[MFTF] AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest failed because of bad design --- ...ediaGalleryImageDetailsSaveActionGroup.xml | 2 +- ...nhancedMediaGalleryImageActionsSection.xml | 4 +- ...diaGalleryEditImageDetailsFromGridTest.xml | 52 ++++++++++++++++ .../AdminMediaGalleryEditImageDetailsTest.xml | 7 ++- ...aGalleryEditImageDetailsFromDialogTest.xml | 61 +++++++++++++++++++ ...daloneMediaGalleryEditImageDetailsTest.xml | 7 ++- 6 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml index 69e8d94522cde..0da3de9501c13 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -19,6 +19,6 @@ <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.title}}" userInput="{{image.title}}" stepKey="setTitle" /> <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.description}}" userInput="{{image.description}}" stepKey="setDescription" /> <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.save}}" stepKey="saveDetails"/> - <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index 1a8f6f553d4ce..17c3e82144d6f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -11,8 +11,8 @@ <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[text()='View Details']" timeout="30"/> - <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> - <element name="edit" type="button" selector="[data-ui-id='action-edit']"/> + <element name="delete" type="button" selector="//ul[@class='action-menu _active']//a[text()='Delete']"/> + <element name="edit" type="button" selector="//ul[@class='action-menu _active']//a[text()='Edit']"/> <element name="imageInGrid" type="button" selector="//li[@data-ui-id='title'and text()='{{imageTitle}}']/parent::*/parent::*/parent::div//img[@class='media-gallery-image-column']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml new file mode 100644 index 0000000000000..91a17a7c1167c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.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="AdminMediaGalleryEditImageDetailsFromGridTest"> + <annotations> + <features value="MediaGalleryUi"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <title value="User edits image meta data in media gallery"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml index 960443998d010..34c3159ab769e 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryEditImageDetailsTest"> + <test name="AdminMediaGalleryEditImageDetailsTest" deprecated="Use AdminMediaGalleryEditImageDetailsFromGridTest instead"> <annotations> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> - <title value="User edits image meta data in media gallery"/> + <title value="DEPRECATED. User edits image meta data in media gallery"/> <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> <description value="User edits image meta data in Standalone Media Gallery"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryEditImageDetailsFromGridTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml new file mode 100644 index 0000000000000..250b42c5510a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml @@ -0,0 +1,61 @@ +<?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="AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest"> + <annotations> + <features value="MediaGalleryUi"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in standalone media gallery"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <generateDate date="now" format="s" stepKey="secondsFromMinuteStart"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> + + <executeJS function="return 60 - {$secondsFromMinuteStart} + 5" stepKey="calcWaitPeriod"/> + <wait time="$calcWaitPeriod" stepKey="waitTillEndOfAMinute"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml index 58c6f32b8d72f..039e9212945e2 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <test name="AdminStandaloneMediaGalleryEditImageDetailsTest" deprecated="Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead"> <annotations> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> - <title value="User edits image meta data in standalone media gallery"/> + <title value="DEPRECATED. User edits image meta data in standalone media gallery"/> <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> <description value="User edits image meta data in Standalone Media Gallery"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> From 458ac648faab828c1b225a2c94d6db1edd5ca936 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Tue, 3 Nov 2020 12:19:03 -0600 Subject: [PATCH 142/195] [AWS S3] Simple cache (#6299) --- app/code/Magento/AwsS3/Driver/AwsS3.php | 57 +- .../Magento/AwsS3/Driver/AwsS3Factory.php | 24 +- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 34 +- app/code/Magento/AwsS3/composer.json | 3 +- .../Driver/DriverFactoryInterface.php | 2 + composer.json | 3 +- composer.lock | 838 +++++++++--------- pub/media/sitemap/.htaccess | 8 +- 8 files changed, 455 insertions(+), 514 deletions(-) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 5dc1f7e8cb216..d0c054b637530 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -8,7 +8,7 @@ namespace Magento\AwsS3\Driver; use Exception; -use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\AdapterInterface; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; @@ -32,30 +32,38 @@ class AwsS3 implements RemoteDriverInterface private const CONFIG = ['ACL' => 'private']; /** - * @var AwsS3Adapter + * @var AdapterInterface */ private $adapter; + /** + * @var LoggerInterface + */ + private $logger; + /** * @var array */ private $streams = []; /** - * @var LoggerInterface + * @var string */ - private $logger; + private $objectUrl; /** - * @param AwsS3Adapter $adapter + * @param AdapterInterface $adapter * @param LoggerInterface $logger + * @param string $objectUrl */ public function __construct( - AwsS3Adapter $adapter, - LoggerInterface $logger + AdapterInterface $adapter, + LoggerInterface $logger, + string $objectUrl ) { $this->adapter = $adapter; $this->logger = $logger; + $this->objectUrl = $objectUrl; } /** @@ -282,26 +290,27 @@ public function getAbsolutePath($basePath, $path, $scheme = null) * @param string $path Relative path * @return string Absolute path */ - private function normalizeAbsolutePath(string $path = '.'): string + private function normalizeAbsolutePath(string $path = '/'): string { $path = ltrim($path, '/'); - $path = str_replace( - $this->adapter->getClient()->getObjectUrl( - $this->adapter->getBucket(), - $this->adapter->applyPathPrefix('.') - ), - '', - $path - ); + $path = str_replace($this->getObjectUrl(''), '', $path); if (!$path) { - $path = '.'; + $path = '/'; } - return $this->adapter->getClient()->getObjectUrl( - $this->adapter->getBucket(), - $this->adapter->applyPathPrefix($path) - ); + return $this->getObjectUrl($path); + } + + /** + * Retrieves object URL from cache. + * + * @param string $path + * @return string + */ + private function getObjectUrl(string $path): string + { + return $this->objectUrl . ltrim($path, '/'); } /** @@ -312,11 +321,7 @@ private function normalizeAbsolutePath(string $path = '.'): string */ private function normalizeRelativePath(string $path): string { - return str_replace( - $this->normalizeAbsolutePath(), - '', - $path - ); + return str_replace($this->normalizeAbsolutePath(), '', $path); } /** diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index 2042e10090407..87efd7c13f398 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -10,6 +10,7 @@ use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; use Magento\Framework\ObjectManagerInterface; +use Magento\RemoteStorage\Driver\DriverException; use Magento\RemoteStorage\Driver\DriverFactoryInterface; use Magento\RemoteStorage\Driver\RemoteDriverInterface; @@ -32,11 +33,7 @@ public function __construct(ObjectManagerInterface $objectManager) } /** - * Creates an instance of AWS S3 driver. - * - * @param array $config - * @param string $prefix - * @return RemoteDriverInterface + * @inheritDoc */ public function create(array $config, string $prefix): RemoteDriverInterface { @@ -46,17 +43,18 @@ public function create(array $config, string $prefix): RemoteDriverInterface unset($config['credentials']); } + if (empty($config['bucket']) || empty($config['region'])) { + throw new DriverException(__('Bucket and region are required values')); + } + + $client = new S3Client($config); + $adapter = new AwsS3Adapter($client, $config['bucket'], $prefix); + return $this->objectManager->create( AwsS3::class, [ - 'adapter' => $this->objectManager->create( - AwsS3Adapter::class, - [ - 'client' => $this->objectManager->create(S3Client::class, ['args' => $config]), - 'bucket' => $config['bucket'], - 'prefix' => $prefix - ] - ) + 'adapter' => $adapter, + 'objectUrl' => $client->getObjectUrl($adapter->getBucket(), $adapter->applyPathPrefix('.')) ] ); } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index 173143b709519..20bc28be4583c 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -7,8 +7,8 @@ namespace Magento\AwsS3\Test\Unit\Driver; -use Aws\S3\S3ClientInterface; use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\Cached\CachedAdapter; use Magento\AwsS3\Driver\AwsS3; use Magento\Framework\Exception\FileSystemException; use PHPUnit\Framework\MockObject\MockObject; @@ -32,41 +32,15 @@ class AwsS3Test extends TestCase */ private $adapterMock; - /** - * @var S3ClientInterface|MockObject - */ - private $clientMock; - - /** - * @var LoggerInterface - */ - private $logger; - /** * @inheritDoc */ protected function setUp(): void { - $this->adapterMock = $this->createMock(AwsS3Adapter::class); - $this->clientMock = $this->getMockForAbstractClass(S3ClientInterface::class); - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); - - $this->adapterMock->method('applyPathPrefix') - ->willReturnArgument(0); - $this->adapterMock->method('getBucket') - ->willReturn('test'); - $this->adapterMock->method('getClient') - ->willReturn($this->clientMock); - $this->clientMock->method('getObjectUrl') - ->willReturnCallback(function (string $bucket, string $path) { - if ($path === '.') { - $path = ''; - } - - return self::URL . $path; - }); + $this->adapterMock = $this->createMock(CachedAdapter::class); + $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $this->driver = new AwsS3($this->adapterMock, $this->logger); + $this->driver = new AwsS3($this->adapterMock, $loggerMock, self::URL); } /** diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json index ce5396223f58d..6e72ac37f8ba6 100644 --- a/app/code/Magento/AwsS3/composer.json +++ b/app/code/Magento/AwsS3/composer.json @@ -9,7 +9,8 @@ "magento/framework": "^100.0.2", "magento/module-remote-storage": "*", "league/flysystem": "^1.0", - "league/flysystem-aws-s3-v3": "^1.0" + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php index 5268cbaea4a77..b9074efc527f0 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -18,6 +18,8 @@ interface DriverFactoryInterface * @param array $config * @param string $prefix * @return RemoteDriverInterface + * + * @throws DriverException */ public function create(array $config, string $prefix): RemoteDriverInterface; } diff --git a/composer.json b/composer.json index 985bf0d9e16ea..b5a484d3828b8 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,8 @@ "webonyx/graphql-php": "^0.13.8", "wikimedia/less.php": "~1.8.0", "league/flysystem": "^1.0", - "league/flysystem-aws-s3-v3": "^1.0" + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0" }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", diff --git a/composer.lock b/composer.lock index f7e0df29a8159..8f855e574a834 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3eb0d410285c05a9f2649b65d8b9a1d5", + "content-hash": "50fd3418a729ef9b577d214fe6c9b0b1", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.158.7", + "version": "3.158.16", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "9afc422ad4ef0f1de9fa2a0be4b856c1dfa31123" + "reference": "6e8fc20ff7bc21b28e80815a9818b6ca7928ae3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9afc422ad4ef0f1de9fa2a0be4b856c1dfa31123", - "reference": "9afc422ad4ef0f1de9fa2a0be4b856c1dfa31123", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6e8fc20ff7bc21b28e80815a9818b6ca7928ae3a", + "reference": "6e8fc20ff7bc21b28e80815a9818b6ca7928ae3a", "shasum": "" }, "require": { @@ -89,7 +89,7 @@ "s3", "sdk" ], - "time": "2020-10-15T18:16:19+00:00" + "time": "2020-10-28T20:19:05+00:00" }, { "name": "colinmollenhour/cache-backend-file", @@ -309,16 +309,16 @@ }, { "name": "composer/composer", - "version": "1.10.15", + "version": "1.10.16", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12" + "reference": "217f0272673c72087862c40cf91ac07eb438d778" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/547c9ee73fe26c77af09a0ea16419176b1cdbd12", - "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12", + "url": "https://api.github.com/repos/composer/composer/zipball/217f0272673c72087862c40cf91ac07eb438d778", + "reference": "217f0272673c72087862c40cf91ac07eb438d778", "shasum": "" }, "require": { @@ -399,7 +399,7 @@ "type": "tidelift" } ], - "time": "2020-10-13T13:59:09+00:00" + "time": "2020-10-24T07:55:59+00:00" }, { "name": "composer/semver", @@ -552,16 +552,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6e076a124f7ee146f2487554a94b6a19a74887ba", + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba", "shasum": "" }, "require": { @@ -606,7 +606,7 @@ "type": "tidelift" } ], - "time": "2020-08-19T10:27:58+00:00" + "time": "2020-10-24T12:39:10+00:00" }, { "name": "container-interop/container-interop", @@ -2060,23 +2060,23 @@ }, { "name": "laminas/laminas-i18n", - "version": "2.10.3", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-i18n.git", - "reference": "94ff957a1366f5be94f3d3a9b89b50386649e3ae" + "reference": "85678f444b6dcb48e8a04591779e11c24e5bb901" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/94ff957a1366f5be94f3d3a9b89b50386649e3ae", - "reference": "94ff957a1366f5be94f3d3a9b89b50386649e3ae", + "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/85678f444b6dcb48e8a04591779e11c24e5bb901", + "reference": "85678f444b6dcb48e8a04591779e11c24e5bb901", "shasum": "" }, "require": { "ext-intl": "*", "laminas/laminas-stdlib": "^2.7 || ^3.0", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ~8.0.0" }, "conflict": { "phpspec/prophecy": "<1.9.0" @@ -2090,10 +2090,10 @@ "laminas/laminas-config": "^2.6", "laminas/laminas-eventmanager": "^2.6.2 || ^3.0", "laminas/laminas-filter": "^2.6.1", - "laminas/laminas-servicemanager": "^2.7.5 || ^3.0.3", + "laminas/laminas-servicemanager": "^3.2.1", "laminas/laminas-validator": "^2.6", "laminas/laminas-view": "^2.6.3", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16" + "phpunit/phpunit": "^9.3" }, "suggest": { "laminas/laminas-cache": "Laminas\\Cache component", @@ -2107,10 +2107,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" - }, "laminas": { "component": "Laminas\\I18n", "config-provider": "Laminas\\I18n\\ConfigProvider" @@ -2131,7 +2127,13 @@ "i18n", "laminas" ], - "time": "2020-03-29T12:51:08+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-10-24T13:14:32+00:00" }, { "name": "laminas/laminas-inputfilter", @@ -3589,18 +3591,65 @@ "description": "Flysystem adapter for the AWS S3 SDK v3.x", "time": "2020-10-08T18:58:37+00:00" }, + { + "name": "league/flysystem-cached-adapter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-cached-adapter.git", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "psr/cache": "^1.0.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7", + "predis/predis": "~1.0", + "tedivm/stash": "~0.12" + }, + "suggest": { + "ext-phpredis": "Pure C implemented extension for PHP" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Cached\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "frankdejonge", + "email": "info@frenky.net" + } + ], + "description": "An adapter decorator to enable meta-data caching.", + "time": "2020-07-25T15:56:04+00:00" + }, { "name": "league/mime-type-detection", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28" + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ea2fbfc988bade315acd5967e6d02274086d0f28", - "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa", "shasum": "" }, "require": { @@ -3638,7 +3687,7 @@ "type": "tidelift" } ], - "time": "2020-09-21T18:10:53+00:00" + "time": "2020-10-18T11:50:25+00:00" }, { "name": "magento/composer", @@ -4374,6 +4423,52 @@ ], "time": "2020-09-08T04:24:43+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.0.0", @@ -4793,16 +4888,16 @@ }, { "name": "symfony/console", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124" + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/90933b39c7b312fc3ceaa1ddeac7eb48cb953124", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124", + "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", "shasum": "" }, "require": { @@ -4837,11 +4932,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -4880,31 +4970,26 @@ "type": "tidelift" } ], - "time": "2020-09-15T07:58:55+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\CssSelector\\": "" @@ -4947,20 +5032,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd" + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e17bb5e0663dc725f7cdcafc932132735b4725cd", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4204f13d2d0b7ad09454f221bb2195fccdf1fe98", + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98", "shasum": "" }, "require": { @@ -4989,11 +5074,6 @@ "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -5032,7 +5112,7 @@ "type": "tidelift" } ], - "time": "2020-09-18T14:07:46+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5112,16 +5192,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae" + "reference": "df08650ea7aee2d925380069c131a66124d79177" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/1a8697545a8d87b9f2f6b1d32414199cc5e20aae", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/df08650ea7aee2d925380069c131a66124d79177", + "reference": "df08650ea7aee2d925380069c131a66124d79177", "shasum": "" }, "require": { @@ -5129,11 +5209,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -5172,31 +5247,26 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:02:37+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/finder", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8" + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", + "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" @@ -5235,24 +5305,24 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -5260,7 +5330,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5311,26 +5381,25 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251" + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": ">=7.1", "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php70": "^1.10", "symfony/polyfill-php72": "^1.10" }, "suggest": { @@ -5339,7 +5408,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5396,24 +5465,24 @@ "type": "tidelift" } ], - "time": "2020-08-04T06:02:08+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + "reference": "727d1096295d807c309fb01a851577302394c897" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -5421,7 +5490,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5477,24 +5546,24 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-mbstring": "For best performance" @@ -5502,7 +5571,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5554,106 +5623,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0|~9.99", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5704,29 +5696,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5780,29 +5772,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", "shasum": "" }, "require": { - "php": ">=7.0.8" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5860,31 +5852,26 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/process", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "9b887acc522935f77555ae8813495958c7771ba7" + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/9b887acc522935f77555ae8813495958c7771ba7", - "reference": "9b887acc522935f77555ae8813495958c7771ba7", + "url": "https://api.github.com/repos/symfony/process/zipball/2f4b049fb80ca5e9874615a2a85dc2a502090f05", + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05", "shasum": "" }, "require": { "php": ">=7.1.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -5923,7 +5910,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:08:58+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/service-contracts", @@ -6692,16 +6679,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.8", + "version": "4.1.9", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "41036e8af66e727c4587012f0366b7f0576a99da" + "reference": "5782e342b978a3efd0b7a776b7808902840b8213" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/41036e8af66e727c4587012f0366b7f0576a99da", - "reference": "41036e8af66e727c4587012f0366b7f0576a99da", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5782e342b978a3efd0b7a776b7808902840b8213", + "reference": "5782e342b978a3efd0b7a776b7808902840b8213", "shasum": "" }, "require": { @@ -6713,7 +6700,7 @@ "ext-json": "*", "ext-mbstring": "*", "guzzlehttp/psr7": "~1.4", - "php": ">=5.6.0 <8.0", + "php": ">=5.6.0 <9.0", "symfony/console": ">=2.7 <6.0", "symfony/css-selector": ">=2.7 <6.0", "symfony/event-dispatcher": ">=2.7 <6.0", @@ -6779,26 +6766,26 @@ "type": "open_collective" } ], - "time": "2020-10-11T17:54:58+00:00" + "time": "2020-10-23T17:59:47+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.13.1", + "version": "1.13.2", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10" + "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/263ef0b7eff80643e82f4cf55351eca553a09a10", - "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6", + "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6", "shasum": "" }, "require": { "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", "ext-dom": "*", - "php": ">=5.6.0 <8.0" + "php": ">=5.6.0 <9.0" }, "type": "library", "autoload": { @@ -6829,33 +6816,30 @@ "keywords": [ "codeception" ], - "time": "2020-08-28T07:49:36+00:00" + "time": "2020-10-21T16:26:20+00:00" }, { "name": "codeception/module-asserts", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667" + "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/32e5be519faaeb60ed3692383dcd1b3390ec2667", - "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de", + "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de", "shasum": "" }, "require": { "codeception/codeception": "*@dev", "codeception/lib-asserts": "^1.13.1", - "php": ">=5.6.0 <8.0" + "php": ">=5.6.0 <9.0" }, "conflict": { "codeception/codeception": "<4.0" }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" - }, "type": "library", "autoload": { "classmap": [ @@ -6885,7 +6869,7 @@ "asserts", "codeception" ], - "time": "2020-08-28T08:06:29+00:00" + "time": "2020-10-21T16:48:15+00:00" }, { "name": "codeception/module-sequence", @@ -6932,26 +6916,23 @@ }, { "name": "codeception/module-webdriver", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "d055c645f600e991e33d1f289a9645eee46c384e" + "reference": "b7dc227f91730e7abb520439decc9ad0677b8a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/d055c645f600e991e33d1f289a9645eee46c384e", - "reference": "d055c645f600e991e33d1f289a9645eee46c384e", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/b7dc227f91730e7abb520439decc9ad0677b8a55", + "reference": "b7dc227f91730e7abb520439decc9ad0677b8a55", "shasum": "" }, "require": { "codeception/codeception": "^4.0", - "php": ">=5.6.0 <8.0", + "php": ">=5.6.0 <9.0", "php-webdriver/webdriver": "^1.6.0" }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" - }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" }, @@ -6983,7 +6964,7 @@ "browser-testing", "codeception" ], - "time": "2020-10-11T18:54:47+00:00" + "time": "2020-10-24T15:41:19+00:00" }, { "name": "codeception/phpunit-wrapper", @@ -7213,16 +7194,16 @@ }, { "name": "doctrine/annotations", - "version": "1.10.4", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", "shasum": "" }, "require": { @@ -7232,13 +7213,14 @@ }, "require-dev": { "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9.x-dev" + "dev-master": "1.11.x-dev" } }, "autoload": { @@ -7273,13 +7255,13 @@ } ], "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "keywords": [ "annotations", "docblock", "parser" ], - "time": "2020-08-10T19:35:50+00:00" + "time": "2020-10-26T10:28:16+00:00" }, { "name": "doctrine/cache", @@ -7592,27 +7574,27 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.4", + "version": "v2.16.7", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4e35806a6d7d8510d6842ae932e8832363d22c87", + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87", "shasum": "" }, "require": { - "composer/semver": "^1.4", + "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^1.2", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.6 || ^7.0", + "php": "^7.1", "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", "symfony/finder": "^3.0 || ^4.0 || ^5.0", @@ -7625,14 +7607,14 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.2", + "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.1", + "php-coveralls/php-coveralls": "^2.4.1", "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", + "phpunitgoodpractices/traits": "^1.9.1", "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, @@ -7685,7 +7667,7 @@ "type": "github" } ], - "time": "2020-06-27T23:57:46+00:00" + "time": "2020-10-27T22:44:27+00:00" }, { "name": "hoa/consistency", @@ -9781,16 +9763,16 @@ }, { "name": "phpunit/php-text-template", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84" + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/18c887016e60e52477e54534956d7b47bc52cd84", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { @@ -9832,7 +9814,7 @@ "type": "github" } ], - "time": "2020-09-28T06:03:05+00:00" + "time": "2020-10-26T05:33:50+00:00" }, { "name": "phpunit/php-timer", @@ -10043,52 +10025,6 @@ ], "time": "2020-05-22T13:54:05+00:00" }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "time": "2016-08-06T20:24:11+00:00" - }, { "name": "psr/simple-cache", "version": "1.0.1", @@ -10139,16 +10075,16 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab" + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/59236be62b1bb9919e6d7f60b0b832dc05cef9ab", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { @@ -10187,7 +10123,7 @@ "type": "github" } ], - "time": "2020-10-02T14:47:54+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -10242,16 +10178,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120" + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7a8ff306445707539c1a6397372a982a1ec55120", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", "shasum": "" }, "require": { @@ -10308,20 +10244,20 @@ "type": "github" } ], - "time": "2020-09-30T06:47:25+00:00" + "time": "2020-10-26T15:49:45+00:00" }, { "name": "sebastian/diff", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092" + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ffc949a1a2aae270ea064453d7535b82e4c32092", - "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", "shasum": "" }, "require": { @@ -10370,7 +10306,7 @@ "type": "github" } ], - "time": "2020-09-28T05:32:55+00:00" + "time": "2020-10-26T13:10:38+00:00" }, { "name": "sebastian/environment", @@ -10607,16 +10543,16 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f6f5957013d84725427d361507e13513702888a4" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f6f5957013d84725427d361507e13513702888a4", - "reference": "f6f5957013d84725427d361507e13513702888a4", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { @@ -10656,20 +10592,20 @@ "type": "github" } ], - "time": "2020-09-28T05:55:06+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { @@ -10707,7 +10643,7 @@ "type": "github" } ], - "time": "2020-09-28T05:56:16+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/phpcpd", @@ -10762,16 +10698,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7" + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", - "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", "shasum": "" }, "require": { @@ -10817,7 +10753,7 @@ "type": "github" } ], - "time": "2020-09-28T05:17:32+00:00" + "time": "2020-10-26T13:17:30+00:00" }, { "name": "sebastian/resource-operations", @@ -10872,16 +10808,16 @@ }, { "name": "sebastian/type", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909" + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fa592377f3923946cb90bf1f6a71ba2e5f229909", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", "shasum": "" }, "require": { @@ -10920,7 +10856,7 @@ "type": "github" } ], - "time": "2020-10-06T08:41:03+00:00" + "time": "2020-10-26T13:18:59+00:00" }, { "name": "sebastian/version", @@ -11044,16 +10980,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.6", + "version": "3.5.8", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "shasum": "" }, "require": { @@ -11091,20 +11027,20 @@ "phpcs", "standards" ], - "time": "2020-08-10T04:50:15+00:00" + "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/config", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191" + "reference": "11baeefa4c179d6908655a7b6be728f62367c193" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/6ad8be6e1280f6734150d8a04a9160dd34ceb191", - "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191", + "url": "https://api.github.com/repos/symfony/config/zipball/11baeefa4c179d6908655a7b6be728f62367c193", + "reference": "11baeefa4c179d6908655a7b6be728f62367c193", "shasum": "" }, "require": { @@ -11128,11 +11064,6 @@ "symfony/yaml": "To use the yaml reference dumper" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Config\\": "" @@ -11171,20 +11102,20 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b" + "reference": "829ca6bceaf68036a123a13a979f3c89289eae78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2dea4a3ef2eb79138354c1d49e9372cc921af20b", - "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/829ca6bceaf68036a123a13a979f3c89289eae78", + "reference": "829ca6bceaf68036a123a13a979f3c89289eae78", "shasum": "" }, "require": { @@ -11217,11 +11148,6 @@ "symfony/yaml": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" @@ -11260,7 +11186,7 @@ "type": "tidelift" } ], - "time": "2020-10-01T12:14:45+00:00" + "time": "2020-10-27T10:11:13+00:00" }, { "name": "symfony/deprecation-contracts", @@ -11328,16 +11254,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b" + "reference": "a2860ec970404b0233ab1e59e0568d3277d32b6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/353b42e7b4fd1c898aab09a059466c9cea74039b", - "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a2860ec970404b0233ab1e59e0568d3277d32b6f", + "reference": "a2860ec970404b0233ab1e59e0568d3277d32b6f", "shasum": "" }, "require": { @@ -11356,11 +11282,6 @@ "symfony/mime": "To use the file extension guesser" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" @@ -11399,20 +11320,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:14:57+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/mime", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "4404d6545125863561721514ad9388db2661eec5" + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/4404d6545125863561721514ad9388db2661eec5", - "reference": "4404d6545125863561721514ad9388db2661eec5", + "url": "https://api.github.com/repos/symfony/mime/zipball/f5485a92c24d4bcfc2f3fc648744fb398482ff1b", + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b", "shasum": "" }, "require": { @@ -11429,11 +11350,6 @@ "symfony/dependency-injection": "^4.4|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Mime\\": "" @@ -11476,20 +11392,20 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd" + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4c7e155bf7d93ea4ba3824d5a14476694a5278dd", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", "shasum": "" }, "require": { @@ -11498,11 +11414,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" @@ -11546,20 +11457,85 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/3d9f57c89011f0266e6b1d469e5c0110513859d5", + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5", "shasum": "" }, "require": { @@ -11567,11 +11543,6 @@ "symfony/service-contracts": "^1.0|^2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Stopwatch\\": "" @@ -11610,20 +11581,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/yaml", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a" + "reference": "f284e032c3cefefb9943792132251b79a6127ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6", "shasum": "" }, "require": { @@ -11644,11 +11615,6 @@ "Resources/bin/yaml-lint" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -11687,20 +11653,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:03:25+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.3.1", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a6b795aeb367c90cc6ed88dadb4cdcac436377c2" + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a6b795aeb367c90cc6ed88dadb4cdcac436377c2", - "reference": "a6b795aeb367c90cc6ed88dadb4cdcac436377c2", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", "shasum": "" }, "require": { @@ -11822,7 +11788,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-10-08T08:40:29+00:00" + "time": "2020-10-28T17:51:34+00:00" }, { "name": "theseer/fdomdocument", diff --git a/pub/media/sitemap/.htaccess b/pub/media/sitemap/.htaccess index b97408bad3f2e..187517e43efb2 100644 --- a/pub/media/sitemap/.htaccess +++ b/pub/media/sitemap/.htaccess @@ -1,7 +1 @@ -<IfVersion < 2.4> - order allow,deny - deny from all -</IfVersion> -<IfVersion >= 2.4> - Require all denied -</IfVersion> +Allow From All From 7c3831f1a745f7420f0ae0db0cd75b3bdac3e889 Mon Sep 17 00:00:00 2001 From: rrego6 <rrego@adobe.com> Date: Tue, 3 Nov 2020 13:48:51 -0500 Subject: [PATCH 143/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Fixed codestyle --- .../framework/Magento/TestFramework/Dependency/PhpRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 88f3c17203120..6ece76690157c 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -423,6 +423,7 @@ private function processStandardUrl(string $path) /** * Create regex patterns from service url paths + * * @return array */ private function getServiceMethodRegexps(): array From 9828790d30ff5f91ebf2e7c0ee4b7da8c6841715 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Tue, 3 Nov 2020 12:50:21 -0600 Subject: [PATCH 144/195] MC-38416: Stabilize CMS tests on S3 (#6309) --- .../Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml index c3d84fafd071c..f3cf259842e1b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml @@ -15,8 +15,9 @@ <arguments> <argument name="FolderName" type="string"/> </arguments> - + <conditionalClick selector="{{MediaGallerySection.StorageRootArrow}}" dependentSelector="{{MediaGallerySection.checkIfArrowExpand}}" stepKey="clickArrowIfClosed" visible="true"/> + <waitForPageLoad time="10" stepKey="waitForDirectoriesTreeBuilding"/> <waitForText userInput="{{FolderName}}" stepKey="waitForNewFolder"/> <click userInput="{{FolderName}}" stepKey="clickOnCreatedFolder"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> From dbe27efe99796d5fd125865528d582c70e7df681 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 3 Nov 2020 16:18:48 -0600 Subject: [PATCH 145/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Category/Product/Action/Rows.php | 77 +++++++++++------- .../Indexer/Product/Category/Action/Rows.php | 81 ++++++++++++------- 2 files changed, 101 insertions(+), 57 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index f05687c53edb5..d98f19ff95fec 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -17,6 +17,7 @@ use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Indexer\Model\WorkingStateProvider; @@ -110,44 +111,64 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByCategories = array_unique($this->limitationByCategories); $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); - $workingState = $this->workingStateProvider->isWorking($indexer->getId()); + $workingState = $this->getWorkingState(); - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->getIndexTable($store->getId())); + if (!$indexer->isScheduled() + || ($indexer->isScheduled() && !$useTempTable) + || ($indexer->isScheduled() && $useTempTable && !$workingState)) { + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->getIndexTable($store->getId())); + } + } else { + $this->removeEntries(); } - } else { - $this->removeEntries(); - } - $this->reindex(); - - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]); - $this->connection->delete( - $this->tableMaintainer->getMainTable($store->getId()), - ['category_id IN (?)' => $removalCategoryIds] - ); - $select = $this->connection->select() - ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); - $this->connection->query( - $this->connection->insertFromSelect( - $select, + $this->reindex(); + + // get actual state + $workingState = $this->getWorkingState(); + + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]); + $this->connection->delete( $this->tableMaintainer->getMainTable($store->getId()), - [], - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + ['category_id IN (?)' => $removalCategoryIds] + ); + $select = $this->connection->select() + ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->query( + $this->connection->insertFromSelect( + $select, + $this->tableMaintainer->getMainTable($store->getId()), + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } } - } - $this->registerCategories($entityIds); - $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + $this->registerCategories($entityIds); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + } return $this; } + /** + * Get state for current and shared indexer + * + * @return bool + */ + private function getWorkingState() : bool + { + $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); + $sharedIndexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); + return $this->workingStateProvider->isWorking($indexer->getId()) + || $this->workingStateProvider->isWorking($sharedIndexer->getId()); + } + /** * Register categories assigned to products * diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 9c17304e12613..48a90ed9d3451 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -17,6 +17,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Indexer\Model\WorkingStateProvider; @@ -104,46 +105,68 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); - $workingState = $this->workingStateProvider->isWorking($indexer->getId()); + $workingState = $this->getWorkingState(); - $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); + if (!$indexer->isScheduled() + || ($indexer->isScheduled() && !$useTempTable) + || ($indexer->isScheduled() && $useTempTable && !$workingState)) { - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->getIndexTable($store->getId())); + $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); + + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->getIndexTable($store->getId())); + } + } else { + $this->removeEntries(); } - } else { - $this->removeEntries(); - } - $this->reindex(); - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $this->connection->delete( - $this->tableMaintainer->getMainTable($store->getId()), - ['product_id IN (?)' => $this->limitationByProducts] - ); - $select = $this->connection->select() - ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); - $this->connection->query( - $this->connection->insertFromSelect( - $select, + $this->reindex(); + + // get actual state + $workingState = $this->getWorkingState(); + + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->delete( $this->tableMaintainer->getMainTable($store->getId()), - [], - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + ['product_id IN (?)' => $this->limitationByProducts] + ); + $select = $this->connection->select() + ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->query( + $this->connection->insertFromSelect( + $select, + $this->tableMaintainer->getMainTable($store->getId()), + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } } - } - $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); + $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); - $this->registerProducts($idsToBeReIndexed); - $this->registerCategories($affectedCategories); - $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + $this->registerProducts($idsToBeReIndexed); + $this->registerCategories($affectedCategories); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + } return $this; } + /** + * Get state for current and shared indexer + * + * @return bool + */ + private function getWorkingState() : bool + { + $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); + $sharedIndexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); + return $this->workingStateProvider->isWorking($indexer->getId()) + || $this->workingStateProvider->isWorking($sharedIndexer->getId()); + } + /** * Get IDs of parent products by their child IDs. * From b5fb919e20609bdf032eb0ba36096fe0d7c1a583 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 3 Nov 2020 16:24:31 -0600 Subject: [PATCH 146/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index e2f739067e19d..455155505110d 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -187,6 +187,7 @@ function ($elem) { $this->indexerFactoryMock, $this->indexersFactoryMock, $this->viewProcessorMock, + $this->workingStateProvider, $makeSharedValidMock ); $model->reindexAllInvalid(); From 10060fb1344a9dd2310e2b7cc3f8ca8d0ed4c21d Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 3 Nov 2020 17:09:47 -0600 Subject: [PATCH 147/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Processor.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 7cab6764e5163..78b8fa070b155 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -42,11 +42,6 @@ class Processor */ protected $mviewProcessor; - /** - * @var WorkingStateProvider - */ - private $workingStateProvider; - /** * @var MakeSharedIndexValid */ @@ -57,7 +52,6 @@ class Processor * @param IndexerInterfaceFactory $indexerFactory * @param Indexer\CollectionFactory $indexersFactory * @param ProcessorInterface $mviewProcessor - * @param WorkingStateProvider $workingStateProvider * @param MakeSharedIndexValid|null $makeSharedValid */ public function __construct( @@ -65,7 +59,6 @@ public function __construct( IndexerInterfaceFactory $indexerFactory, Indexer\CollectionFactory $indexersFactory, ProcessorInterface $mviewProcessor, - WorkingStateProvider $workingStateProvider, MakeSharedIndexValid $makeSharedValid = null ) { $this->config = $config; @@ -73,7 +66,6 @@ public function __construct( $this->indexersFactory = $indexersFactory; $this->mviewProcessor = $mviewProcessor; $this->makeSharedValid = $makeSharedValid ?: ObjectManager::getInstance()->get(MakeSharedIndexValid::class); - $this->workingStateProvider = $workingStateProvider; } /** From 8ea52e3da85ae97f027827008888a70b146469ee Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 3 Nov 2020 17:56:23 -0600 Subject: [PATCH 148/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Test/Unit/Model/ProcessorTest.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index 455155505110d..f55eb8c11e2e9 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -16,7 +16,6 @@ use Magento\Indexer\Model\Indexer\Collection; use Magento\Indexer\Model\Indexer\CollectionFactory; use Magento\Indexer\Model\Indexer\State; -use Magento\Indexer\Model\WorkingStateProvider; use Magento\Indexer\Model\Processor; use Magento\Indexer\Model\Processor\MakeSharedIndexValid; use PHPUnit\Framework\MockObject\MockObject; @@ -49,16 +48,9 @@ class ProcessorTest extends TestCase */ protected $viewProcessorMock; - /** - * @var WorkingStateProvider|MockObject - */ - private $workingStateProvider; protected function setUp(): void { - $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) - ->disableOriginalConstructor() - ->getMock(); $this->configMock = $this->getMockForAbstractClass( ConfigInterface::class, [], @@ -82,12 +74,19 @@ protected function setUp(): void '', false ); + + $indexerRegistryMock = $this->getIndexRegistryMock([]); + $makeSharedValidMock = new MakeSharedIndexValid( + $this->configMock, + $indexerRegistryMock + ); + $this->model = new Processor( $this->configMock, $this->indexerFactoryMock, $this->indexersFactoryMock, $this->viewProcessorMock, - $this->workingStateProvider + $makeSharedValidMock ); } @@ -187,7 +186,6 @@ function ($elem) { $this->indexerFactoryMock, $this->indexersFactoryMock, $this->viewProcessorMock, - $this->workingStateProvider, $makeSharedValidMock ); $model->reindexAllInvalid(); From c3fa46dcfda64f676ed602560e6d14a4cb7053b7 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 3 Nov 2020 17:57:35 -0600 Subject: [PATCH 149/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Catalog/Model/Indexer/Product/Category/Action/Rows.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 48a90ed9d3451..ab04f7c56c3db 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -97,6 +97,7 @@ public function __construct( * @return $this * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface * @throws \DomainException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute(array $entityIds = [], $useTempTable = false) { @@ -105,7 +106,7 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); - $workingState = $this->getWorkingState(); + $workingState = $this->isWorkingState(); if (!$indexer->isScheduled() || ($indexer->isScheduled() && !$useTempTable) @@ -123,7 +124,7 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->reindex(); // get actual state - $workingState = $this->getWorkingState(); + $workingState = $this->isWorkingState(); if ($useTempTable && !$workingState && $indexer->isScheduled()) { foreach ($this->storeManager->getStores() as $store) { @@ -159,7 +160,7 @@ public function execute(array $entityIds = [], $useTempTable = false) * * @return bool */ - private function getWorkingState() : bool + private function isWorkingState() : bool { $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); $sharedIndexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); From 78b353e6443c40b5c5d7bcf39275f215557c98f1 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 3 Nov 2020 17:59:40 -0600 Subject: [PATCH 150/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Catalog/Model/Indexer/Category/Product/Action/Rows.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index d98f19ff95fec..c53277a58157d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -111,7 +111,7 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByCategories = array_unique($this->limitationByCategories); $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); - $workingState = $this->getWorkingState(); + $workingState = $this->isWorkingState(); if (!$indexer->isScheduled() || ($indexer->isScheduled() && !$useTempTable) @@ -127,7 +127,7 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->reindex(); // get actual state - $workingState = $this->getWorkingState(); + $workingState = $this->isWorkingState(); if ($useTempTable && !$workingState && $indexer->isScheduled()) { foreach ($this->storeManager->getStores() as $store) { @@ -161,7 +161,7 @@ public function execute(array $entityIds = [], $useTempTable = false) * * @return bool */ - private function getWorkingState() : bool + private function isWorkingState() : bool { $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); $sharedIndexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); From 010211f03a0a6e4072f871aafa1336c634e7a9f2 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 4 Nov 2020 09:17:52 +0200 Subject: [PATCH 151/195] MC-38893: Avoid BIC making introduced const DDL_EXISTS private in lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php --- lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index c5e17a97c9f01..53f09fda19471 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -56,7 +56,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface public const DDL_CREATE = 2; public const DDL_INDEX = 3; public const DDL_FOREIGN_KEY = 4; - public const DDL_EXISTS = 5; + private const DDL_EXISTS = 5; public const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; public const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; From ba82de1f5db35ed522926dc3977f36b30dde520f Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 4 Nov 2020 09:38:26 +0200 Subject: [PATCH 152/195] MC-38894: [MFTF] AdminMediaGalleryCatalogUiEditCategoryGridPageTest failed because of bad design --- ...yCatalogUiEditCategoryFromGridPageTest.xml | 39 +++++++++++++++++++ ...lleryCatalogUiEditCategoryGridPageTest.xml | 7 +++- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml new file mode 100644 index 0000000000000..2beb0ad12e5d0 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml @@ -0,0 +1,39 @@ +<?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="AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="User Edits Category from Category grid"/> + <description value="Edit Category from Media Gallery Category Grid"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5034526"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> + <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"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetGridFilters"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AdminAssertCategoryPageTitleActionGroup" stepKey="assertCategoryByName"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml index 2a606d8ab6a9e..739b25d1ce0ed 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest"> + <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest" deprecated="Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead"> <annotations> <features value="AdminMediaGalleryCategoryGrid"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> - <title value="User Edits Category from Category grid"/> + <title value="DEPRECATED. User Edits Category from Category grid"/> <stories value="Story 58: User sees entities where asset is used in" /> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5034526"/> <description value="Edit Category from Media Gallery Category Grid"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> From 375a49ff47d3a74112618830c12af6b7178667f3 Mon Sep 17 00:00:00 2001 From: engcom-Kilo <mikola.malevanec@transoftgroup.com> Date: Tue, 3 Nov 2020 17:54:38 +0200 Subject: [PATCH 153/195] MC-38833: Region field visible for Country when "Allow to Choose State if It is Optional for Country" is disabled. --- .../view/base/web/js/form/element/region.js | 46 +++++++++++++++---- .../Ui/base/js/form/element/region.test.js | 34 ++++++++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index cd9c2aee85dc6..68b480d25a38c 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -23,6 +23,22 @@ define([ } }, + /** + * {@inheritdoc} + */ + initialize: function () { + var option; + + this._super(); + + option = _.find(this.countryOptions, function (row) { + return row['is_default'] === true; + }); + this.hideRegion(option); + + return this; + }, + /** * Method called every time country selector's value gets changed. * Updates all validations and requirements for certain country. @@ -42,16 +58,9 @@ define([ return; } - defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); - - if (option['is_region_visible'] === false) { - // Hide select and corresponding text input field if region must not be shown for selected country. - this.setVisible(false); + this.hideRegion(option); - if (this.customEntry) { // eslint-disable-line max-depth - this.toggleInput(false); - } - } + defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); isRegionRequired = !this.skipValidation && !!option['is_region_required']; @@ -67,7 +76,24 @@ define([ input.validation['required-entry'] = isRegionRequired; input.validation['validate-not-number-first'] = !this.options().length; }.bind(this)); + }, + + /** + * Hide select and corresponding text input field if region must not be shown for selected country. + * + * @private + * @param {Object}option + */ + hideRegion: function (option) { + if (!option || option['is_region_visible'] !== false) { + return; + } + + this.setVisible(false); + + if (this.customEntry) { + this.toggleInput(false); + } } }); }); - diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js index a957db5d1c119..517e13281d402 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js @@ -47,6 +47,40 @@ define([ }); }); + describe('initialize method', function () { + it('Hides region field when it should be hidden for default country', function () { + model.countryOptions = { + 'DefaultCountryCode': { + 'is_default': true, + 'is_region_visible': false + }, + 'NonDefaultCountryCode': { + 'is_region_visible': true + } + }; + + model.initialize(); + + expect(model.visible()).toEqual(false); + }); + + it('Shows region field when it should be visible for default country', function () { + model.countryOptions = { + 'CountryCode': { + 'is_default': true, + 'is_region_visible': true + }, + 'NonDefaultCountryCode': { + 'is_region_visible': false + } + }; + + model.initialize(); + + expect(model.visible()).toEqual(true); + }); + }); + describe('update method', function () { it('makes field optional when there is no corresponding country', function () { var value = 'Value'; From 304e5a1b09bd5a256e4a907821e4f92864c1e86a Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Wed, 4 Nov 2020 12:10:57 +0200 Subject: [PATCH 154/195] MC-37816: Performance - endless scheduled export of catalog with 100k+ products --- .../Export/Product/RowCustomizerTest.php | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php new file mode 100644 index 0000000000000..110451aa19f1a --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableImportExport\Test\Unit\Model\Export\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Downloadable\Model\LinkRepository; +use Magento\Downloadable\Model\Product\Type as Type; +use Magento\Downloadable\Model\SampleRepository; +use Magento\DownloadableImportExport\Model\Export\RowCustomizer; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class to test Customizes output during export + */ +class RowCustomizerTest extends TestCase +{ + /** + * @var LinkRepository|MockObject + */ + private $linkRepository; + + /** + * @var SampleRepository|MockObject + */ + private $sampleRepository; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var RowCustomizer + */ + private $rowCustomizer; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->linkRepository = $this->createMock(LinkRepository::class); + $this->sampleRepository = $this->createMock(SampleRepository::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + + $this->rowCustomizer = new RowCustomizer( + $this->storeManager, + $this->linkRepository, + $this->sampleRepository + ); + } + + /** + * Test to Prepare downloadable data for export + */ + public function testPrepareData() + { + $productIds = [1, 2, 3]; + $collection = $this->createMock(ProductCollection::class); + $collection->expects($this->at(0)) + ->method('addAttributeToFilter') + ->with('entity_id', ['in' => $productIds]) + ->willReturnSelf(); + $collection->expects($this->at(1)) + ->method('addAttributeToFilter') + ->with('type_id', ['eq' => Type::TYPE_DOWNLOADABLE]) + ->willReturnSelf(); + $collection->method('addAttributeToSelect')->willReturnSelf(); + $collection->method('getIterator')->willReturn(new \ArrayIterator([])); + + $this->storeManager->expects($this->once()) + ->method('setCurrentStore') + ->with(Store::DEFAULT_STORE_ID); + + $this->rowCustomizer->prepareData($collection, $productIds); + } +} From 8f2a8be7962bcceb12790805b3171f874abdf5d4 Mon Sep 17 00:00:00 2001 From: Serhii Bohomaz <serhii.bohomaz@transoftgroup.com> Date: Wed, 4 Nov 2020 13:07:30 +0200 Subject: [PATCH 155/195] MC-37545: Create automated test for "Edit Category on Store View Level" --- .../Catalog/Block/Category/TitleTest.php | 136 ++++++++++++++++++ .../Category/CategoryUrlRewriteTest.php | 108 ++++++++++++++ .../Model/Category/DataProviderTest.php | 77 +++++++--- .../_files/category_on_second_store.php | 29 ++++ .../category_on_second_store_rollback.php | 11 ++ .../Controller/Store/SwitchActionTest.php | 126 ++++++++++++---- 6 files changed, 441 insertions(+), 46 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php new file mode 100644 index 0000000000000..7bc359935bf60 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use PHPUnit\Framework\TestCase; + +/** + * Category title check + * + * @magentoAppArea frontend + * @see \Magento\Theme\Block\Html\Title + */ +class TitleTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var Registry */ + private $registry; + + /** @var PageFactory */ + private $pageFactory; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + + parent::tearDown(); + } + + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Store/_files/store.php + * @return void + */ + public function testCategoryNameOnStoreView(): void + { + $id = 333; + $categoryNameForSecondStore = 'Category Name For Second Store'; + $this->executeInStoreContext->execute( + 'test', + [$this, 'updateCategoryName'], + $this->categoryRepository->get($id), + $categoryNameForSecondStore + ); + $this->registerCategory($this->categoryRepository->get($id)); + $this->assertStringContainsString('Category 1', $this->getBlockTitle(), 'Wrong category name'); + $this->registerCategory($this->categoryRepository->get($id, $this->storeManager->getStore('test')->getId())); + $this->assertStringContainsString($categoryNameForSecondStore, $this->getBlockTitle(), 'Wrong category name'); + } + + /** + * Update category name + * + * @param CategoryInterface $category + * @param string $categoryName + * @return void + */ + public function updateCategoryName(CategoryInterface $category, string $categoryName): void + { + $category->setName($categoryName); + $this->categoryRepository->save($category); + } + + /** + * Get title block + * + * @return string + */ + private function getBlockTitle(): string + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'catalog_category_view', + ]); + $page->getLayout()->generateXml(); + $block = $page->getLayout()->getBlock('page.main.title'); + $this->assertNotFalse($block); + + return $block->stripTags($block->toHtml()); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('current_category'); + $this->registry->register('current_category', $category); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php index ad62a4ec2df29..931bbf835521e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php @@ -7,16 +7,25 @@ namespace Magento\Catalog\Controller\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Layer\Category; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Session as CatalogSession; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Response\Http; use Magento\Framework\Registry; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\Response; +use Magento\TestFramework\Store\ExecuteInStoreContext; use Magento\TestFramework\TestCase\AbstractController; /** * Checks category availability on storefront by url rewrite * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @magentoDbIsolation enabled */ @@ -31,6 +40,18 @@ class CategoryUrlRewriteTest extends AbstractController /** @var string */ private $categoryUrlSuffix; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var CatalogSession */ + private $catalogSession; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -44,6 +65,10 @@ protected function setUp(): void CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE ); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->catalogSession = $this->_objectManager->get(CatalogSession::class); + $this->executeInStoreContext = $this->_objectManager->get(ExecuteInStoreContext::class); } /** @@ -87,4 +112,87 @@ public function categoryRewriteProvider(): array ], ]; } + + /** + * Test category url on different store view + * + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Store/_files/store.php + * @return void + */ + public function testCategoryUrlOnStoreView(): void + { + $id = 333; + $secondStoreUrlKey = 'category-1-second'; + $currentStore = $this->storeManager->getStore(); + $secondStore = $this->storeManager->getStore('test'); + $this->executeInStoreContext->execute( + $secondStore, + [$this, 'updateCategoryUrlKey'], + $id, + (int)$secondStore->getId(), + $secondStoreUrlKey + ); + $url = sprintf('/' . $secondStoreUrlKey . '%s', $this->categoryUrlSuffix); + $this->executeInStoreContext->execute($secondStore, [$this, 'dispatch'], $url); + $this->assertCategoryIsVisible(); + $this->assertEquals( + $secondStoreUrlKey, + $this->categoryRepository->get($id, (int)$secondStore->getId())->getUrlKey(), + 'Wrong category is registered' + ); + $this->cleanUpCachedObjects(); + $defaultStoreUrlKey = $this->categoryRepository->get($id, $currentStore->getId())->getUrlKey(); + $this->dispatch(sprintf($defaultStoreUrlKey . '%s', $this->categoryUrlSuffix)); + $this->assertCategoryIsVisible(); + } + + /** + * Assert that category is available in storefront + * + * @return void + */ + private function assertCategoryIsVisible(): void + { + $this->assertEquals( + Response::STATUS_CODE_200, + $this->getResponse()->getHttpResponseCode(), + 'Wrong response code is returned' + ); + $this->assertNotNull((int)$this->catalogSession->getData('last_viewed_category_id')); + } + + /** + * Clean up cached objects + * + * @return void + */ + private function cleanUpCachedObjects(): void + { + $this->catalogSession->clearStorage(); + $this->registry->unregister('current_category'); + $this->registry->unregister('category'); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_objectManager->removeSharedInstance(Response::class); + $this->_objectManager->removeSharedInstance(Resolver::class); + $this->_objectManager->removeSharedInstance(Category::class); + $this->_objectManager->removeSharedInstance('categoryFilterList'); + $this->_response = null; + $this->_request = null; + } + + /** + * Update category url key + * + * @param int $id + * @param int $storeId + * @param string $categoryUrlKey + * @return void + */ + public function updateCategoryUrlKey(int $id, int $storeId, string $categoryUrlKey): void + { + $category = $this->categoryRepository->get($id, $storeId); + $category->setUrlKey($categoryUrlKey); + $this->categoryRepository->save($category); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php index 1d846fc154fc0..6ae6669956f62 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php @@ -3,14 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; use Magento\Catalog\Model\CategoryFactory; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; @@ -19,42 +22,36 @@ use PHPUnit\Framework\TestCase; /** + * Testing category form data provider. + * * @magentoDbIsolation enabled * @magentoAppIsolation enabled * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataProviderTest extends TestCase { - /** - * @var DataProvider - */ + /** @var DataProvider */ private $dataProvider; - /** - * @var Registry - */ + /** @var Registry */ private $registry; - /** - * @var CategoryFactory - */ + /** @var CategoryFactory */ private $categoryFactory; - /** - * @var CategoryLayoutUpdateManager - */ + /** @var CategoryLayoutUpdateManager */ private $fakeFiles; - /** - * @var ScopeConfigInterface - */ + /** @var ScopeConfigInterface */ private $scopeConfig; - /** - * @var StoreManagerInterface - */ + /** @var StoreManagerInterface */ private $storeManager; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + /** * Create subject instance. * @@ -80,8 +77,7 @@ protected function setUp(): void $objectManager = Bootstrap::getObjectManager(); $objectManager->configure([ 'preferences' => [ - \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class - => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + LayoutUpdateManager::class => CategoryLayoutUpdateManager::class ] ]); parent::setUp(); @@ -91,6 +87,15 @@ protected function setUp(): void $this->fakeFiles = $objectManager->get(CategoryLayoutUpdateManager::class); $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); $this->storeManager = $objectManager->get(StoreManagerInterface::class); + $this->categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('category'); } /** @@ -267,7 +272,7 @@ public function testExistingCategoryLayoutUnaffectedByDefaults(): void /** * Check if category page layout default value setting will apply to the new category during it's creation * - * @throws NoSuchEntityException + * @return void */ public function testNewCategoryLayoutMatchesDefault(): void { @@ -288,4 +293,32 @@ public function testNewCategoryLayoutMatchesDefault(): void $this->assertEquals($categoryDefaultPageLayout, $categoryPageLayout); } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_on_second_store.php + * @return void + */ + public function testCategoryStoreView(): void + { + $id = 333; + $secondStore = $this->storeManager->getStore('test'); + $category = $this->categoryRepository->get($id, $secondStore->getId()); + $this->registerCategory($category); + $data = $this->dataProvider->getData(); + $this->assertNotEmpty($data); + $this->assertEquals('Category 1 Second', $data[$id]['name']); + $this->assertEquals('category-1-second-url-key', $data[$id]['url_key']); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('category'); + $this->registry->register('category', $category); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php new file mode 100644 index 0000000000000..0b094ba29290e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/store.php'); + +$objectManager = Bootstrap::getObjectManager(); +$storeManager = $objectManager->get(StoreManagerInterface::class); +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +$executeInStoreContext = $objectManager->get(ExecuteInStoreContext::class); + +$currentStore = $storeManager->getStore(); +$secondStore = $storeManager->getStore('test'); +$category = $categoryRepository->get(333); +$category->setName('Category 1 Second'); +$category->setUrlKey('category-1-second-url-key'); +$executeInStoreContext->execute($secondStore, function ($categoryRepository, $category) { + $categoryRepository->save($category); +}, $categoryRepository, $category); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php new file mode 100644 index 0000000000000..b7b8491612fec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index e4d78de54d308..c506b77e45442 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -3,9 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Store\Controller\Store; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Response\RedirectInterface; use Magento\Framework\Encryption\UrlCoder; use Magento\Framework\Interception\InterceptorInterface; use Magento\Store\Api\StoreResolverInterface; @@ -16,8 +21,11 @@ use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractController; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use Magento\Store\Model\ResourceModel\Store as StoreResource; /** * Test for store switch controller. @@ -27,23 +35,42 @@ */ class SwitchActionTest extends AbstractController { - /** - * @var RedirectDataPreprocessorInterface - */ + /** @var RedirectDataPreprocessorInterface */ private $preprocessor; - /** - * @var MockObject - */ + + /** @var MockObject */ private $preprocessorMock; - /** - * @var RedirectDataPostprocessorInterface - */ + + /** @var RedirectDataPostprocessorInterface */ private $postprocessor; - /** - * @var MockObject - */ + + /** @var MockObject */ private $postprocessorMock; + /** @var RedirectDataGenerator */ + private $redirectDataGenerator; + + /** @var ContextInterfaceFactory */ + private $contextFactory; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var UrlCoder */ + private $urlEncoder; + + /** @var RedirectInterface */ + private $redirect; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreResource */ + private $storeResource; + + /** @var StoreInterfaceFactory */ + private $storeFactory; + /** * @inheritDoc */ @@ -53,10 +80,17 @@ protected function setUp(): void $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); - $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); $this->_objectManager->addSharedInstance($this->postprocessorMock, $this->getClassName($this->postprocessor)); + $this->redirectDataGenerator = $this->_objectManager->get(RedirectDataGenerator::class); + $this->contextFactory = $this->_objectManager->get(ContextInterfaceFactory::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->urlEncoder = $this->_objectManager->get(UrlCoder::class); + $this->redirect = $this->_objectManager->get(RedirectInterface::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeResource = $this->_objectManager->get(StoreResource::class); + $this->storeFactory = $this->_objectManager->get(StoreInterfaceFactory::class); } /** @@ -80,8 +114,9 @@ protected function tearDown(): void * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + * @return void */ - public function testSwitch() + public function testSwitch(): void { $data = ['key1' => 'value1', 'key2' => 1]; $this->preprocessorMock->method('process') @@ -131,6 +166,7 @@ function (ContextInterface $context) { * Return class name of the given object * * @param mixed $instance + * @return string */ private function getClassName($instance): string { @@ -150,19 +186,20 @@ private function getClassName($instance): string * incorrect work of page cache. * * @magentoDbIsolation enabled + * @return void */ - public function testExecuteWithCustomDefaultStore() + public function testExecuteWithCustomDefaultStore(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + Bootstrap::getInstance()->reinitialize(); $defaultStoreCode = 'default'; $modifiedDefaultCode = 'modified_default_code'; $this->changeStoreCode($defaultStoreCode, $modifiedDefaultCode); $this->dispatch('stores/store/switch'); - /** @var \Magento\Framework\App\Http\Context $httpContext */ - $httpContext = $this->_objectManager->get(\Magento\Framework\App\Http\Context::class); - $httpContext->unsValue(\Magento\Store\Model\Store::ENTITY); - $this->assertEquals($modifiedDefaultCode, $httpContext->getValue(\Magento\Store\Model\Store::ENTITY)); + /** @var Context $httpContext */ + $httpContext = $this->_objectManager->get(Context::class); + $httpContext->unsValue(Store::ENTITY); + $this->assertEquals($modifiedDefaultCode, $httpContext->getValue(Store::ENTITY)); $this->changeStoreCode($modifiedDefaultCode, $defaultStoreCode); } @@ -172,13 +209,54 @@ public function testExecuteWithCustomDefaultStore() * * @param string $from * @param string $to + * @return void */ - private function changeStoreCode($from, $to) + private function changeStoreCode(string $from, string $to): void { /** @var Store $store */ - $store = $this->_objectManager->create(Store::class); - $store->load($from, 'code'); + $store = $this->storeFactory->create(); + $this->storeResource->load($store, $from, 'code'); $store->setCode($to); - $store->save(); + $this->storeResource->save($store); + } + + /** + * Switch to category on second store + * + * @magentoDataFixture Magento/Catalog/_files/category_on_second_store.php + * @magentoDbIsolation disabled + * @return void + */ + public function testSwitchToCategoryOnSecondStore(): void + { + $id = 333; + $fromStore = $this->storeManager->getStore(); + $targetStore = $this->storeManager->getStore('test'); + $category = $this->categoryRepository->get($id, $fromStore->getId()); + + $redirectData = $this->redirectDataGenerator->generate( + $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $this->redirect->getRedirectUrl(), + ] + ) + ); + + $this->getRequest()->setParams( + [ + '___from_store' => $fromStore->getCode(), + StoreManagerInterface::PARAM_NAME => $targetStore->getCode(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($category->getUrl()), + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), + ] + ); + + $this->dispatch('stores/store/switch'); + $categorySecond = $this->categoryRepository->get($id, $targetStore->getId()); + $this->assertRedirect($this->stringContains($categorySecond->getUrlKey())); } } From b44339725c65a6047e007bd50267d19fc6551c4a Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 4 Nov 2020 13:19:55 +0200 Subject: [PATCH 156/195] MC-37902: Create automated test for "Paging and sort by function on widget grid" --- .../Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php index 3e01305d9f39f..57d4322ded9a3 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php @@ -128,7 +128,7 @@ public function gridFiltersDataProvider(): array ), ], 'expected_widgets' => [ - 'recently compared products' + 'recently compared products', ], ], ]; @@ -147,7 +147,6 @@ public function testGridSorting(array $filter, array $expectedWidgets): void { $this->request->setParams($filter); $collection = $this->getGridCollection(); - $this->assertCount(count($expectedWidgets), $collection); $this->assertEquals($expectedWidgets, $collection->getColumnValues('title')); } From b8ec1a32e7a589fbe6377523088df07ce714ca43 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Wed, 4 Nov 2020 15:37:07 +0200 Subject: [PATCH 157/195] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../{Address.php => AddressDataProvider.php} | 19 +-- ...rder_create_load_block_billing_address.xml | 2 +- .../templates/order/create/form/address.phtml | 10 +- .../Adminhtml/Order/Create/ReorderTest.php | 153 ++++++++++++++++++ 4 files changed, 164 insertions(+), 20 deletions(-) rename app/code/Magento/Sales/ViewModel/Customer/Address/Billing/{Address.php => AddressDataProvider.php} (62%) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php diff --git a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php similarity index 62% rename from app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php rename to app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php index a7ec8e6587d70..c539e965b9df9 100644 --- a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php +++ b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php @@ -9,17 +9,16 @@ use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Sales\Model\AdminOrder\Create; -use Magento\Quote\Model\Quote\Address as QuoteAddress; /** - * Customer address formatter + * Customer billing address data provider */ -class Address implements ArgumentInterface +class AddressDataProvider implements ArgumentInterface { /** * @var Create */ - protected $orderCreate; + private $orderCreate; /** * Customer billing address @@ -32,16 +31,6 @@ public function __construct( $this->orderCreate = $orderCreate; } - /** - * Return billing address object - * - * @return QuoteAddress - */ - public function getAddress(): QuoteAddress - { - return $this->orderCreate->getBillingAddress(); - } - /** * Get save billing address in the address book * @@ -49,6 +38,6 @@ public function getAddress(): QuoteAddress */ public function getSaveInAddressBook(): int { - return (int)$this->getAddress()->getSaveInAddressBook(); + return (int)$this->orderCreate->getBillingAddress()->getSaveInAddressBook(); } } diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index edefa8de55c7a..91148d86055fc 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -12,7 +12,7 @@ <arguments> <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> - <argument name="customerBillingAddress" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\Address</argument> + <argument name="billingAddressDataProvider" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\AddressDataProvider</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index bdb1a6c8cba94..3b6b789cabccf 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -25,9 +25,9 @@ endif; $customerAddressFormatter = $block->getData('customerAddressFormatter'); /** - * @var \Magento\Sales\ViewModel\Customer\Address\Billing\Address $billingAddress + * @var \Magento\Sales\ViewModel\Customer\Address\Billing\AddressDataProvider $billingAddressDataProvider */ -$billingAddress = $block->getData('customerBillingAddress'); +$billingAddressDataProvider = $block->getData('billingAddressDataProvider'); /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| @@ -119,9 +119,11 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if ($billingAddress && $billingAddress->getSaveInAddressBook()): ?> + <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook()): ?> checked="checked" - <?php elseif ($block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + <?php elseif ($block->getIsShipping() + && !$block->getDontSaveInAddressBook() + && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php new file mode 100644 index 0000000000000..0856e58c308d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Create; + +use Magento\Backend\Model\Session\Quote; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Registry; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\OrderFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Test load block for order create controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Index + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ReorderTest extends AbstractBackendController +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var CustomerInterfaceFactory + */ + private $customerFactory; + + /** + * @var AccountManagementInterface + */ + private $accountManagement; + + /** + * @var OrderFactory + */ + private $orderFactory; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var array + */ + private $customerIds = []; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class); + $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); + $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + foreach ($this->customerIds as $customerId) { + try { + $this->customerRepository->deleteById($customerId); + } catch (NoSuchEntityException $e) { + //customer already deleted + } + } + } + + /** + * Test load billing address by reorder for delegating customer + * + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_address.php + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testLoadBillingAddressAfterReorderWithDelegatingCustomer(): void + { + $orderId = $this->getOrderWithDelegatingCustomer()->getId(); + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/loadBlock/block/billing_address'); + $html = $this->getResponse()->getBody(); + $this->assertEquals( + 0, + Xpath::getElementsCountForXpath( + '//*[@id="order-billing_address_save_in_address_book" and contains(@checked, "checked")]', + $html + ), + 'Billing address checked "Save in address book"' + ); + } + + /** + * Get Order with delegating customer + * + * @return OrderInterface + */ + private function getOrderWithDelegatingCustomer(): OrderInterface + { + $orderAutoincrementId = '100000001'; + /** @var Order $orderModel */ + $orderModel = $this->orderFactory->create(); + $orderModel->loadByIncrementId($orderAutoincrementId); + //Saving new customer with prepared data from order. + /** @var CustomerInterface $customer */ + $customer = $this->customerFactory->create(); + $customer->setWebsiteId(1) + ->setEmail('customer_order_delegate@example.com') + ->setGroupId(1) + ->setStoreId(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setTaxvat('12') + ->setGender(0); + $createdCustomer = $this->accountManagement->createAccount( + $customer, + '12345abcD' + ); + $this->customerIds[] = $createdCustomer->getId(); + $orderModel->setCustomerId($createdCustomer->getId()); + + return $this->orderRepository->save($orderModel); + } +} From 3a64d1457a8d552d7183a528d45b93fc86c3c95f Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Wed, 4 Nov 2020 08:57:56 -0600 Subject: [PATCH 158/195] MC-38835: Fix failing random tests (#6312) --- .../testsuite/Magento/Framework/Error/ProcessorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php index 3a2a02a0a5776..917b79588312c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php @@ -150,6 +150,6 @@ public function testGetViewFileUrl(): void $this->processor->_errorDir = __DIR__ . '/version2/magento2'; $this->assertStringNotContainsString('version2/magento2', $this->processor->getViewFileUrl()); - $this->assertStringContainsString('pub/errors/', $this->processor->getViewFileUrl()); + $this->assertStringContainsString('errors/', $this->processor->getViewFileUrl()); } } From 077942845fd53df322f10b9c8b9527458c13d3cf Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 09:41:16 -0600 Subject: [PATCH 159/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Processor.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 78b8fa070b155..c8578d3ec06bb 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -85,7 +85,18 @@ public function reindexAllInvalid() // Skip indexers having shared index that was already complete $sharedIndex = $indexerConfig['shared_index'] ?? null; if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { + if (!empty($sharedIndex)) { + $sharedIndexer = $this->indexerFactory->create()->load($sharedIndex); + if ($sharedIndexer->getView()->isEnabled()) { + $sharedIndexer->getView()->suspend(); + } + } $indexer->reindexAll(); + if (!empty($sharedIndex)) { + if ($sharedIndexer->getView()->isEnabled()) { + $sharedIndexer->getView()->resume(); + } + } if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { $this->sharedIndexesComplete[] = $sharedIndex; From c02fa441dadea2a88688ba6f0722ebd513eac3b6 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Wed, 4 Nov 2020 18:31:27 +0200 Subject: [PATCH 160/195] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../adminhtml/templates/order/create/form/address.phtml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 3b6b789cabccf..69b26d70e684a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -119,11 +119,8 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook()): ?> - checked="checked" - <?php elseif ($block->getIsShipping() - && !$block->getDontSaveInAddressBook() - && !$block->getAddressId()): ?> + <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook() || + $block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> From bdf9e6b370db408b456cb62ef4af2fe72da9c219 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 4 Nov 2020 19:21:08 +0200 Subject: [PATCH 161/195] MC-38893: Avoid BIC making introduced const DDL_EXISTS private in lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php --- lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 53f09fda19471..5765a3a7fe1b2 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -665,11 +665,9 @@ protected function _prepareQuery(&$sql, &$bind = []) } // Mixed bind is not supported - so remember whether it is named bind, to normalize later if required - $isNamedBind = false; if ($bind) { foreach ($bind as $k => $v) { if (!is_int($k)) { - $isNamedBind = true; if ($k[0] != ':') { $bind[":{$k}"] = $v; unset($bind[$k]); From 0fd59b6aec3833fa47f26488aea3cbb4e94051b1 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 11:41:36 -0600 Subject: [PATCH 162/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Processor.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index c8578d3ec06bb..78b8fa070b155 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -85,18 +85,7 @@ public function reindexAllInvalid() // Skip indexers having shared index that was already complete $sharedIndex = $indexerConfig['shared_index'] ?? null; if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { - if (!empty($sharedIndex)) { - $sharedIndexer = $this->indexerFactory->create()->load($sharedIndex); - if ($sharedIndexer->getView()->isEnabled()) { - $sharedIndexer->getView()->suspend(); - } - } $indexer->reindexAll(); - if (!empty($sharedIndex)) { - if ($sharedIndexer->getView()->isEnabled()) { - $sharedIndexer->getView()->resume(); - } - } if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { $this->sharedIndexesComplete[] = $sharedIndex; From c546c8e9ec7b1c2a142dea59428424b8c768ad08 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 11:51:53 -0600 Subject: [PATCH 163/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index f55eb8c11e2e9..bbb74812d99a3 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -48,7 +48,6 @@ class ProcessorTest extends TestCase */ protected $viewProcessorMock; - protected function setUp(): void { $this->configMock = $this->getMockForAbstractClass( From 6f399488797511efe40a9da5207726ca17d41f71 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 13:08:47 -0600 Subject: [PATCH 164/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Indexer.php | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 2ca08fb35cefd..2a1e165b67f2e 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -417,6 +417,16 @@ public function reindexAll() $state = $this->getState(); $state->setStatus(StateInterface::STATUS_WORKING); $state->save(); + + $sharedIndexers = []; + $indexerConfig = $this->config->getIndexer($this->getId()); + if ($indexerConfig['shared_index'] !== null) { + $sharedIndexers = $this->getSharedIndexers($indexerConfig['shared_index']); + } + if (!empty($sharedIndexers)) { + $this->suspendSharedViews($sharedIndexers); + } + if ($this->getView()->isEnabled()) { $this->getView()->suspend(); } @@ -424,16 +434,73 @@ public function reindexAll() $this->getActionInstance()->executeFull(); $state->setStatus(StateInterface::STATUS_VALID); $state->save(); + if (!empty($sharedIndexers)) { + $this->resumeSharedViews($sharedIndexers); + } $this->getView()->resume(); } catch (\Throwable $exception) { $state->setStatus(StateInterface::STATUS_INVALID); $state->save(); + if (!empty($sharedIndexers)) { + $this->resumeSharedViews($sharedIndexers); + } $this->getView()->resume(); throw $exception; } } } + /** + * Get indexer ids that uses same index + * + * @param string $sharedIndex + * @return array + */ + private function getSharedIndexers(string $sharedIndex) : array + { + $result = []; + foreach (array_keys($this->config->getIndexers()) as $indexerId) { + if ($indexerId === $this->getId()) { + continue; + } + $indexerConfig = $this->config->getIndexer($indexerId); + if ($indexerConfig['shared_index'] === $sharedIndex) { + $indexer = $this->indexersFactory->create(); + $indexer->load($indexerId); + $result[] = $indexer; + } + } + return $result; + } + + /** + * Suspend views of shared indexers + * + * @param array $sharedIndexers + * @return void + */ + private function suspendSharedViews(array $sharedIndexers) : void + { + foreach ($sharedIndexers as $indexer) { + if ($indexer->getView()->isEnabled()) { + $indexer->getView()->suspend(); + } + } + } + + /** + * Suspend views of shared indexers + * + * @param array $sharedIndexers + * @return void + */ + private function resumeSharedViews(array $sharedIndexers) : void + { + foreach ($sharedIndexers as $indexer) { + $indexer->getView()->resume(); + } + } + /** * Regenerate one row in index by ID * From e36335421e3f10f9c37e6849f572d11252c0e184 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 13:19:04 -0600 Subject: [PATCH 165/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Category/Product/Action/RowsTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php index 48cd2de65945e..f53b05a88c54f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Indexer\Category\Product\Action\Rows; use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Indexer\Model\WorkingStateProvider; use Magento\Framework\EntityManager\EntityMetadataInterface; @@ -219,10 +220,26 @@ public function testExecuteWithIndexerWorking() : void $this->connection->expects($this->any()) ->method('fetchOne') ->willReturn($categoryId); - $this->indexerRegistry->expects($this->any()) + $this->indexerRegistry->expects($this->at(0)) ->method('get') ->with(ProductCategoryIndexer::INDEXER_ID) ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(1)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(2)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(3)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(4)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); $this->indexer->expects($this->any()) ->method('getId') ->willReturn(ProductCategoryIndexer::INDEXER_ID); From e06f08e556213b1dd37661541727543bfc329590 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 13:24:58 -0600 Subject: [PATCH 166/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Product/Category/Action/RowsTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php index 6750ccd60775c..66eb058c7b0a4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php @@ -12,6 +12,7 @@ use Magento\Store\Model\Store; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Indexer\Product\Category\Action\Rows; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Indexer\Model\WorkingStateProvider; @@ -228,10 +229,26 @@ public function testExecuteWithIndexerWorking() : void $this->connection->expects($this->any()) ->method('fetchOne') ->willReturn($categoryId); - $this->indexerRegistry->expects($this->any()) + $this->indexerRegistry->expects($this->at(0)) ->method('get') ->with(CategoryProductIndexer::INDEXER_ID) ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(1)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(2)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(3)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(4)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); $this->indexer->expects($this->any()) ->method('getId') ->willReturn(CategoryProductIndexer::INDEXER_ID); From eb1af2153885d37f4b0b79306a10f22e5c231065 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 14:45:45 -0600 Subject: [PATCH 167/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Model/Indexer.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 2a1e165b67f2e..ac8b9590e58f4 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -13,6 +13,7 @@ use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Indexer\StateInterface; use Magento\Framework\Indexer\StructureFactory; +use Magento\Framework\Indexer\IndexerInterfaceFactory; /** * Indexer model. @@ -66,6 +67,11 @@ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface */ private $workingStateProvider; + /** + * @var IndexerInterfaceFactory + */ + private $indexerFactory; + /** * @param ConfigInterface $config * @param ActionFactory $actionFactory @@ -74,6 +80,7 @@ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface * @param Indexer\StateFactory $stateFactory * @param Indexer\CollectionFactory $indexersFactory * @param WorkingStateProvider $workingStateProvider + * @param IndexerInterfaceFactory $indexerFactory * @param array $data */ public function __construct( @@ -84,6 +91,7 @@ public function __construct( Indexer\StateFactory $stateFactory, Indexer\CollectionFactory $indexersFactory, WorkingStateProvider $workingStateProvider, + IndexerInterfaceFactory $indexerFactory, array $data = [] ) { $this->config = $config; @@ -93,6 +101,7 @@ public function __construct( $this->stateFactory = $stateFactory; $this->indexersFactory = $indexersFactory; $this->workingStateProvider = $workingStateProvider; + $this->indexerFactory = $indexerFactory; parent::__construct($data); } @@ -465,7 +474,7 @@ private function getSharedIndexers(string $sharedIndex) : array } $indexerConfig = $this->config->getIndexer($indexerId); if ($indexerConfig['shared_index'] === $sharedIndex) { - $indexer = $this->indexersFactory->create(); + $indexer = $this->indexerFactory->create(); $indexer->load($indexerId); $result[] = $indexer; } From b9b5c507812cb58791fe926255df021a3d2b69b4 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 15:25:18 -0600 Subject: [PATCH 168/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- .../Indexer/Test/Unit/Model/IndexerTest.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 9b2dc52e2f247..2881d2a6da73e 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Indexer\ConfigInterface; use Magento\Framework\Indexer\StateInterface; use Magento\Framework\Indexer\StructureFactory; +use Magento\Framework\Indexer\IndexerInterfaceFactory; use Magento\Framework\Mview\ViewInterface; use Magento\Indexer\Model\Indexer; use Magento\Indexer\Model\Indexer\CollectionFactory; @@ -61,6 +62,11 @@ class IndexerTest extends TestCase */ private $workingStateProvider; + /** + * @var IndexerInterfaceFactory|MockObject + */ + private $indexerFactoryMock; + protected function setUp(): void { $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) @@ -79,6 +85,10 @@ protected function setUp(): void ActionFactory::class, ['create'] ); + $this->indexerFactoryMock = $this->createPartialMock( + IndexerInterfaceFactory::class, + ['create'] + ); $this->viewMock = $this->getMockForAbstractClass( ViewInterface::class, [], @@ -109,7 +119,8 @@ protected function setUp(): void $this->viewMock, $this->stateFactoryMock, $this->indexFactoryMock, - $this->workingStateProvider + $this->workingStateProvider, + $this->indexerFactoryMock ); } @@ -356,7 +367,7 @@ protected function getIndexerData() protected function loadIndexer($indexId) { $this->configMock->expects( - $this->once() + $this->any() )->method( 'getIndexer' )->with( From 6744fdb5183aa2fa10f86cf0baef9562f2a62ccb Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Wed, 4 Nov 2020 10:02:22 -0600 Subject: [PATCH 169/195] MC-38890: Product Images are not updated correctly via API when files from pub/media/ do not exist but the records do exist in gallery media tables - Fix images are not updated if existing images are not in pub/media/catalog/ and new images names match the existing images names --- .../Model/Product/Gallery/UpdateHandler.php | 35 +++++--- .../Product/Gallery/UpdateHandlerTest.php | 86 +++++++++++++++++++ .../Product/UpdateProductWebsiteTest.php | 8 +- 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 8061422d84288..6a1392d776d31 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -77,26 +77,26 @@ protected function processDeletedImages($product, array &$images) { $filesToDelete = []; $recordsToDelete = []; - $picturesInOtherStores = []; $imagesToDelete = []; - - foreach ($this->resourceModel->getProductImages($product, $this->extractStoreIds($product)) as $image) { - $picturesInOtherStores[$image['filepath']] = true; + $imagesToNotDelete = []; + foreach ($images as $image) { + if (empty($image['removed'])) { + $imagesToNotDelete[] = $image['file']; + } } - foreach ($images as &$image) { + foreach ($images as $image) { if (!empty($image['removed'])) { if (!empty($image['value_id'])) { if (preg_match('/\.\.(\\\|\/)/', $image['file'])) { continue; } $recordsToDelete[] = $image['value_id']; - $imagesToDelete[] = $image['file']; - $catalogPath = $this->mediaConfig->getBaseMediaPath(); - $isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']); - // only delete physical files if they are not used by any other products and if this file exist - if ($isFile && !($this->resourceModel->countImageUses($image['file']) > 1)) { - $filesToDelete[] = ltrim($image['file'], '/'); + if (!in_array($image['file'], $imagesToNotDelete)) { + $imagesToDelete[] = $image['file']; + if ($this->canDeleteImage($image['file'])) { + $filesToDelete[] = ltrim($image['file'], '/'); + } } } } @@ -107,6 +107,19 @@ protected function processDeletedImages($product, array &$images) $this->removeDeletedImages($filesToDelete); } + /** + * Check if image exists and is not used by any other products + * + * @param string $file + * @return bool + */ + private function canDeleteImage(string $file): bool + { + $catalogPath = $this->mediaConfig->getBaseMediaPath(); + return $this->mediaDirectory->isFile($catalogPath . $file) + && $this->resourceModel->countImageUses($file) <= 1; + } + /** * @inheritdoc * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 568859c1c83f0..481ec6aeac0f2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -507,6 +507,92 @@ public function testDeleteWithMultiWebsites(): void $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); } + /** + * Check that product images should be updated successfully regardless if the existing images exist or not + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @dataProvider updateImageDataProvider + * @param string $newFile + * @param string $expectedFile + * @param bool $exist + * @return void + */ + public function testUpdateImage(string $newFile, string $expectedFile, bool $exist): void + { + $product = $this->getProduct(Store::DEFAULT_STORE_ID); + $images = $product->getData('media_gallery')['images']; + $this->assertCount(1, $images); + $oldImage = reset($images) ?: []; + $this->assertEquals($oldImage['file'], $product->getImage()); + $this->assertEquals($oldImage['file'], $product->getSmallImage()); + $this->assertEquals($oldImage['file'], $product->getThumbnail()); + $path = $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $oldImage['file']); + $tmpPath = $this->mediaDirectory->getAbsolutePath($this->config->getBaseTmpMediaPath() . $oldImage['file']); + $this->assertFileExists($path); + $this->mediaDirectory->getDriver()->copy($path, $tmpPath); + if (!$exist) { + $this->mediaDirectory->getDriver()->deleteFile($path); + $this->assertFileDoesNotExist($path); + } + // delete old image + $oldImage['removed'] = 1; + $newImage = [ + 'file' => $newFile, + 'position' => 1, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ]; + $newImageRoles = [ + 'image' => $newFile, + 'small_image' => 'no_selection', + 'thumbnail' => 'no_selection', + ]; + $product->setData('media_gallery', ['images' => [$oldImage, $newImage]]); + $product->addData($newImageRoles); + $this->updateHandler->execute($product); + $product = $this->getProduct(Store::DEFAULT_STORE_ID); + $images = $product->getData('media_gallery')['images']; + $this->assertCount(1, $images); + $image = reset($images) ?: []; + $this->assertEquals($newImage['label'], $image['label']); + $this->assertEquals($expectedFile, $product->getImage()); + $this->assertEquals($newImageRoles['small_image'], $product->getSmallImage()); + $this->assertEquals($newImageRoles['thumbnail'], $product->getThumbnail()); + $path = $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $product->getImage()); + // Assert that the image exists on disk. + $this->assertFileExists($path); + } + + /** + * @return array[] + */ + public function updateImageDataProvider(): array + { + return [ + [ + '/m/a/magento_image.jpg', + '/m/a/magento_image_1.jpg', + true + ], + [ + '/m/a/magento_image.jpg', + '/m/a/magento_image.jpg', + false + ], + [ + '/m/a/magento_small_image.jpg', + '/m/a/magento_small_image.jpg', + true + ], + [ + '/m/a/magento_small_image.jpg', + '/m/a/magento_small_image.jpg', + false + ] + ]; + } + /** * Check product image link and product image exist * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php index c63a3c8249e77..e973a25d07354 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php @@ -10,7 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Product\Website\Link; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -82,10 +82,10 @@ public function testUnassignProductFromWebsite(): void */ public function testAssignNonExistingWebsite(): void { - $messageFormat = 'The website with id %s that was requested wasn\'t found. Verify the website and try again.'; + $messageFormat = 'The product was unable to be saved. Please try again.'; $nonExistingWebsiteId = 921564; - $this->expectException(NoSuchEntityException::class); - $this->expectExceptionMessage((string)__(sprintf($messageFormat, $nonExistingWebsiteId))); + $this->expectException(CouldNotSaveException::class); + $this->expectExceptionMessage((string)__($messageFormat)); $this->updateProductWebsites('simple2', [$nonExistingWebsiteId]); } From 82bf51bc062cbcc1d1fb5c293ca5e064dbc1d506 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 4 Nov 2020 16:22:37 -0600 Subject: [PATCH 170/195] MC-38168: Manual Indexer after Merchandising - Empty Catalog/ Number of products incorrect --- app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 2881d2a6da73e..bcdfbea78b0b3 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -357,7 +357,8 @@ protected function getIndexerData() 'view_id' => 'view_test', 'action_class' => 'Some\Class\Name', 'title' => 'Indexer public name', - 'description' => 'Indexer public description' + 'description' => 'Indexer public description', + 'shared_index' => null ]; } From d6ccfb50a4d2d7202cacbf83620ae31d7a5bfc3a Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Tue, 3 Nov 2020 21:51:35 -0600 Subject: [PATCH 171/195] MC-38613: Support by Magento CatalogGraphQl --- app/code/Magento/AwsS3/Driver/AwsS3.php | 6 +++++- .../Model/Product/Gallery/ProcessorTest.php | 7 ++++--- .../Magento/Catalog/Model/ProductTest.php | 2 +- .../Catalog/_files/catalog_category_image.php | 11 ++++++----- .../_files/catalog_tmp_category_image.php | 3 +-- .../Magento/Catalog/_files/product_image.php | 5 ++--- .../Catalog/_files/product_simple_with_image.php | 2 +- .../Catalog/_files/validate_image_info.php | 10 +++++----- .../Catalog/controllers/_files/products.php | 2 +- lib/internal/Magento/Framework/Api/Uploader.php | 16 ---------------- 10 files changed, 26 insertions(+), 38 deletions(-) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index d0c054b637530..8b0469862a4e9 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -344,7 +344,11 @@ public function isFile($path): bool $path = $this->normalizeRelativePath($path); $path = rtrim($path, '/'); - return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_FILE; + if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { + return ($meta['type'] ?? null) === self::TYPE_FILE; + } + + return false; } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php index f836fe9cbb96a..fb384253e27a7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php @@ -46,9 +46,10 @@ public static function setUpBeforeClass(): void $mediaDirectory->create($config->getBaseTmpMediaPath()); $mediaDirectory->create($config->getBaseMediaPath()); - copy($fixtureDir . "/magento_image.jpg", self::$_mediaTmpDir . "/magento_image.jpg"); - copy($fixtureDir . "/magento_image.jpg", self::$_mediaDir . "/magento_image.jpg"); - copy($fixtureDir . "/magento_small_image.jpg", self::$_mediaTmpDir . "/magento_small_image.jpg"); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaTmpDir . "/magento_image.jpg", file_get_contents($fixtureDir . "/magento_image.jpg")); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaDir . "/magento_image.jpg", file_get_contents($fixtureDir . "/magento_image.jpg")); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaTmpDir . "/magento_small_image.jpg", file_get_contents($fixtureDir . "/magento_small_image.jpg")); + } public static function tearDownAfterClass(): void diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index b0f36f250991b..8acb243a706c2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -246,7 +246,7 @@ protected function _copyFileToBaseTmpMediaPath($sourceFile) $mediaDirectory->create($config->getBaseTmpMediaPath()); $targetFile = $config->getTmpMediaPath(basename($sourceFile)); - copy($sourceFile, $mediaDirectory->getAbsolutePath($targetFile)); + $mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($targetFile), file_get_contents($sourceFile)); return $targetFile; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php index 3491065323c9f..7a2ad0fefac8a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php @@ -10,13 +10,14 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ -$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::MEDIA); +$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); $fileName = 'magento_small_image.jpg'; $fileNameLong = 'magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg'; $filePath = 'catalog/category/' . $fileName; $filePathLong = 'catalog/category/' . $fileNameLong; $mediaDirectory->create('catalog/category'); - -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($filePath)); -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileNameLong, $mediaDirectory->getAbsolutePath($filePathLong)); +$shortImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName); +$longImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileNameLong); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePath), $shortImageContent); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePathLong), $longImageContent); +unset($shortImageContent, $longImageContent); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php index 2562acdda2dc3..ce688f38ed1ec 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php @@ -15,5 +15,4 @@ $fileName = 'magento_small_image.jpg'; $tmpFilePath = 'catalog/tmp/category/' . $fileName; $mediaDirectory->create('catalog/tmp/category'); - -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($tmpFilePath)); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($tmpFilePath), file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php index 962a66f11f532..1794530832d04 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php @@ -24,12 +24,11 @@ $images = ['magento_image.jpg', 'magento_small_image.jpg', 'magento_thumbnail.jpg']; foreach ($images as $image) { - $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . DIRECTORY_SEPARATOR . $targetTmpDirPath - . DIRECTORY_SEPARATOR . $image; + $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . $targetTmpDirPath . $image; $sourceFilePath = __DIR__ . DIRECTORY_SEPARATOR . $image; + $mediaDirectory->getDriver()->filePutContents($targetTmpFilePath, file_get_contents($sourceFilePath)); - copy($sourceFilePath, $targetTmpFilePath); // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir $database->saveFile($targetTmpFilePath); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php index 252f99c97b787..688a3bd199570 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php @@ -38,7 +38,7 @@ $mediaDirectory->create($targetTmpDirPath); $dist = $mediaDirectory->getAbsolutePath($mediaConfig->getBaseMediaPath() . DIRECTORY_SEPARATOR . 'magento_image.jpg'); -copy(__DIR__ . '/magento_image.jpg', $dist); +$mediaDirectory->getDriver()->filePutContents($dist, file_get_contents(__DIR__ . '/magento_image.jpg')); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php index 96ddb797a6dea..945f582b8cbdd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php @@ -12,10 +12,10 @@ /** @var Magento\Catalog\Model\Product\Media\Config $config */ $config = $objectManager->get(\Magento\Catalog\Model\Product\Media\Config::class); -/** @var $tmpDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ -$tmpDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); -$tmpDirectory->create($config->getBaseTmpMediaPath()); +/** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ +$mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); +$mediaDirectory->create($config->getBaseTmpMediaPath()); -$targetTmpFilePath = $tmpDirectory->getAbsolutePath($config->getBaseTmpMediaPath() . '/magento_small_image.jpg'); -copy(__DIR__ . '/magento_small_image.jpg', $targetTmpFilePath); +$targetTmpFilePath = $mediaDirectory->getAbsolutePath($config->getBaseTmpMediaPath() . '/magento_small_image.jpg'); +$mediaDirectory->getDriver()->filePutContents($targetTmpFilePath, file_get_contents(__DIR__ . '/magento_small_image.jpg')); // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir diff --git a/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php b/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php index 3878cd2e5176e..4a3c8f2e6b96c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php @@ -24,7 +24,7 @@ $baseTmpMediaPath = $config->getBaseTmpMediaPath(); $mediaDirectory->create($baseTmpMediaPath); -copy(__DIR__ . '/product_image.png', $mediaDirectory->getAbsolutePath($baseTmpMediaPath . '/product_image.png')); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($baseTmpMediaPath . '/product_image.png'), file_get_contents(__DIR__ . '/product_image.png')); /** @var $productOne \Magento\Catalog\Model\Product */ $productOne = $objectManager->create(\Magento\Catalog\Model\Product::class); diff --git a/lib/internal/Magento/Framework/Api/Uploader.php b/lib/internal/Magento/Framework/Api/Uploader.php index 3a4019b9caf84..3f98b38bc2fdf 100644 --- a/lib/internal/Magento/Framework/Api/Uploader.php +++ b/lib/internal/Magento/Framework/Api/Uploader.php @@ -39,20 +39,4 @@ public function processFileAttributes($fileAttributes) $this->_fileExists = true; } } - - /** - * Move files from TMP folder into destination folder - * - * @param string $tmpPath - * @param string $destPath - * @return bool|void - */ - protected function _moveFile($tmpPath, $destPath) - { - if (is_uploaded_file($tmpPath)) { - return move_uploaded_file($tmpPath, $destPath); - } elseif (is_file($tmpPath)) { - return rename($tmpPath, $destPath); - } - } } From f6c739275e5d55065a235edb97f631645286b183 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Wed, 4 Nov 2020 19:52:56 -0600 Subject: [PATCH 172/195] MC-38613: Support by Magento CatalogGraphQl --- .../visual_swatch_attribute_with_different_options_type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index 8d2b427d7f7f3..a4a755c4b92db 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -24,7 +24,7 @@ $imagesGenerator = Bootstrap::getObjectManager()->get(ImagesGenerator::class); /** @var SwatchesMedia $swatchesMedia */ $swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); -$imageName = '/visual_swatch_attribute_option_type_image.jpg'; +$imageName = 'visual_swatch_attribute_option_type_image.jpg'; $imagesGenerator->generate([ 'image-width' => 110, 'image-height' => 90, From b90aa9e01881700ebc5b01a47d3a2b1e9af435a6 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 4 Nov 2020 21:41:07 -0600 Subject: [PATCH 173/195] MC-38914: Image custom attributes are incorrectly escaped --- .../Magento/Catalog/view/frontend/templates/product/image.phtml | 2 +- .../view/frontend/templates/product/image_with_borders.phtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml index 98d17045a1b2d..86b332679bcb4 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml @@ -11,7 +11,7 @@ <!--deprecated template as image_with_borders is a primary one--> <img class="photo image <?= $escaper->escapeHtmlAttr($block->getClass()) ?>" <?php foreach ($block->getCustomAttributes() as $name => $value): ?> - <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtmlAttr($value) ?>" + <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtml($value) ?>" <?php endforeach; ?> src="<?= $escaper->escapeUrl($block->getImageUrl()) ?>" loading="lazy" diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 8abfe368909e4..cc1a7276c70b8 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -15,7 +15,7 @@ $paddingBottom = $block->getRatio() * 100; <span class="product-image-wrapper"> <img class="<?= $escaper->escapeHtmlAttr($block->getClass()) ?>" <?php foreach ($block->getCustomAttributes() as $name => $value): ?> - <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtmlAttr($value) ?>" + <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtml($value) ?>" <?php endforeach; ?> src="<?= $escaper->escapeUrl($block->getImageUrl()) ?>" loading="lazy" From 478c63469ba268b8ab0fd865f3a4cb5e67a30c92 Mon Sep 17 00:00:00 2001 From: rrego6 <rrego@adobe.com> Date: Thu, 5 Nov 2020 00:02:49 -0500 Subject: [PATCH 174/195] MC-24057: Implement static dependency analysis for wildcard and API urls - Removed validation from test webapi loader. --- .../testsuite/Magento/Test/Integrity/DependencyTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index ae79ac70a0c52..ac6bf8a026917 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Config\Reader\Filesystem as Reader; +use Magento\Framework\Config\ValidationState\Configurable; use Magento\Framework\Exception\LocalizedException; use Magento\Test\Integrity\Dependency\Converter; use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; @@ -26,7 +27,6 @@ use Magento\TestFramework\Dependency\ReportsConfigRule; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; -use Magento\TestFramework\Workaround\Override\Config\ValidationState; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -288,12 +288,10 @@ protected static function _initRules() new WebapiFileResolver(self::getComponentRegistrar()), new Converter(), new SchemaLocator(self::getComponentRegistrar()), - new ValidationState(), + new Configurable(false), 'webapi.xml', [ '/routes/route' => ['url', 'method'], - '/routes/route/resources/resource' => 'ref', - '/routes/route/data/parameter' => 'name' ], ); From 8dc9c1a909f442f1f504dbf2353b3bf42b4c770e Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Thu, 5 Nov 2020 12:13:22 +0200 Subject: [PATCH 175/195] MC-37542: Create automated test for "[API] Create CMS page using API service" --- .../Magento/Cms/Api/PageRepositoryTest.php | 159 +++++++++++------- 1 file changed, 97 insertions(+), 62 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index f8d146dcc2bf7..53b1c56616403 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Api; use Magento\Authorization\Model\Role; @@ -191,15 +193,12 @@ public function testGet(): void */ public function testGetByStores(string $requestStore): void { - $page = $this->getPageByIdentifier->execute('page100', 0); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->updatePage($page, ['store_id' => $store->getId()]); - $page = $this->getPageByIdentifier->execute('page100', $store->getId()); - $comparedFields = $this->getPageRequestData()['page']; + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $this->updatePage('page100', 0, ['store_id' => $newStoreId]); + $page = $this->getPageByIdentifier->execute('page100', $newStoreId); $expectedData = array_intersect_key( $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), - $comparedFields + $this->getPageRequestData()['page'] ); $serviceInfo = $this->getServiceInfo( 'GetById', @@ -212,9 +211,7 @@ public function testGetByStores(string $requestStore): void } $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->assertNotNull($page['id']); - $actualData = array_intersect_key($page, $comparedFields); - $this->assertEquals($expectedData, $actualData, 'Error while getting page.'); + $this->assertResponseData($page, $expectedData); } /** @@ -255,27 +252,17 @@ public function testCreate(): void */ public function testCreateByStores(string $requestStore): void { + $newStoreId = $this->getStoreIdByRequestStore($requestStore); $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = $this->getPageRequestData(); + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->assertNotNull($page['id']); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->currentPage = $this->getPageByIdentifier->execute( - $requestData['page'][PageInterface::IDENTIFIER], - $store->getId() - ); - $actualData = array_intersect_key($page, $requestData['page']); - $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); - if ($requestStore != 'all') { - $this->cmsUiDataProvider->addFilter( - $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() - ); - } - $pageGridData = $this->cmsUiDataProvider->getData(); + $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->assertResponseData($page, $requestData['page']); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( $this->isPageInArray($pageGridData['items'], $page['id']), - sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $requestStore) ); } @@ -373,10 +360,8 @@ public function testUpdateOneField(): void */ public function testUpdateByStores(string $requestStore): void { - $page = $this->getPageByIdentifier->execute('page100', 0); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->updatePage($page, ['store_id' => $store->getId()]); + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $page = $this->updatePage('page100', 0, ['store_id' => $newStoreId]); $serviceInfo = $this->getServiceInfo( 'Save', Request::HTTP_METHOD_PUT, @@ -385,22 +370,12 @@ public function testUpdateByStores(string $requestStore): void $requestData = $this->getPageRequestData(); $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->assertNotNull($page['id']); - $this->currentPage = $this->getPageByIdentifier->execute( - $requestData['page'][PageInterface::IDENTIFIER], - $store->getId() - ); - $actualData = array_intersect_key($page, $requestData['page']); - $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); - if ($requestStore != 'all') { - $this->cmsUiDataProvider->addFilter( - $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() - ); - } - $pageGridData = $this->cmsUiDataProvider->getData(); + $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->assertResponseData($page, $requestData['page']); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( $this->isPageInArray($pageGridData['items'], $page['id']), - sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $requestStore) ); } @@ -440,10 +415,8 @@ public function testDelete(): void */ public function testDeleteByStores(string $requestStore): void { - $page = $this->getPageByIdentifier->execute('page100', 0); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->updatePage($page, ['store_id' => $store->getId()]); + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $page = $this->updatePage('page100', 0, ['store_id' => $newStoreId]); $serviceInfo = $this->getServiceInfo( 'DeleteById', Request::HTTP_METHOD_DELETE, @@ -453,17 +426,13 @@ public function testDeleteByStores(string $requestStore): void if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { $requestData[PageInterface::PAGE_ID] = $page->getId(); } + $pageResponse = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); $this->assertTrue($pageResponse); - if ($requestStore != 'all') { - $this->cmsUiDataProvider->addFilter( - $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() - ); - } - $pageGridData = $this->cmsUiDataProvider->getData(); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertFalse( - $this->isPageInArray($pageGridData['items'], $page->getId()), - sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $storeCode) + $this->isPageInArray($pageGridData['items'], (int)$page->getId()), + sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $requestStore) ); } @@ -561,12 +530,12 @@ public function byStoresProvider(): array 'default_store' => [ 'request_store' => 'default', ], - /*'second_store' => [ + 'second_store' => [ 'request_store' => 'fixture_second_store', ], 'all' => [ 'request_store' => 'all', - ],*/ + ], ]; } @@ -606,9 +575,9 @@ private function prepareCmsPages(): array /** * Create page with hard-coded identifier to test with create-delete-create flow. * @param string $identifier - * @return string + * @return int */ - private function createPageWithIdentifier($identifier): string + private function createPageWithIdentifier($identifier): int { $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ @@ -792,12 +761,14 @@ private function isPageInArray(array $pageGridData, int $pageId): bool /** * Update page with data * - * @param PageInterface $page + * @param string $pageIdentifier + * @param int $storeId * @param array $pageData * @return PageInterface */ - private function updatePage(PageInterface $page, array $pageData): PageInterface + private function updatePage(string $pageIdentifier, int $storeId, array $pageData): PageInterface { + $page = $this->getPageByIdentifier->execute($pageIdentifier, $storeId); $page->addData($pageData); return $this->pageRepository->save($page); @@ -820,4 +791,68 @@ private function getPageRequestData(): array ] ]; } + + /** + * Get store id by request store code + * + * @param string $requestStoreCode + * @return int + */ + private function getStoreIdByRequestStore(string $requestStoreCode): int + { + $storeCode = $requestStoreCode === 'all' ? 'admin' : $requestStoreCode; + $store = $this->storeManager->getStore($storeCode); + + return (int)$store->getId(); + } + + /** + * Check that the response data is as expected + * + * @param array $page + * @param array $expectedData + * @return void + */ + private function assertResponseData(array $page, array $expectedData): void + { + $this->assertNotNull($page['id']); + $actualData = array_intersect_key($page, $expectedData); + $this->assertEquals($expectedData, $actualData, 'Response data does not match expected.'); + } + + /** + * Get page grid data of cms ui dataprovider filtering by store code + * + * @param string $requestStore + * @return array + */ + private function getPageGridDataByStoreCode(string $requestStore): array + { + if ($requestStore !== 'all') { + $store = $this->storeManager->getStore($requestStore); + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + + return $this->cmsUiDataProvider->getData(); + } + + /** + * Get page by identifier without throw exception + * + * @param string $identifier + * @param int $storeId + * @return PageInterface|null + */ + private function getPageByIdentifier(string $identifier, int $storeId): ?PageInterface + { + $page = null; + try { + $page = $this->getPageByIdentifier->execute($identifier, $storeId); + } catch (NoSuchEntityException $exception) { + } + + return $page; + } } From 42e7daff1e966e2f95c51ba1665d9b73a9b13fa6 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Thu, 5 Nov 2020 15:11:53 +0200 Subject: [PATCH 176/195] MC-37542: Create automated test for "[API] Create CMS page using API service" --- .../Magento/Cms/Api/PageRepositoryTest.php | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index 53b1c56616403..8751f2a39921d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -13,6 +13,7 @@ use Magento\Authorization\Model\RulesFactory; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\Data\PageInterfaceFactory; +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\Cms\Ui\Component\DataProvider as CmsDataProvider; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; @@ -85,7 +86,7 @@ class PageRepositoryTest extends WebapiAbstract private $adminTokens; /** - * @var array + * @var PageInterface[] */ private $createdPages = []; @@ -110,9 +111,9 @@ class PageRepositoryTest extends WebapiAbstract private $cmsUiDataProvider; /** - * @var GetPageByIdentifierInterface + * @var PageResource */ - private $getPageByIdentifier; + private $pageResource; /** * @inheritdoc @@ -137,7 +138,7 @@ protected function setUp(): void 'requestFieldName' => 'id', ] ); - $this->getPageByIdentifier = $this->objectManager->get(GetPageByIdentifierInterface::class); + $this->pageResource = $this->objectManager->get(PageResource::class); } /** @@ -151,7 +152,9 @@ protected function tearDown(): void } foreach ($this->createdPages as $page) { - $this->pageRepository->delete($page); + if ($page->getId()) { + $this->pageRepository->delete($page); + } } } @@ -195,7 +198,7 @@ public function testGetByStores(string $requestStore): void { $newStoreId = $this->getStoreIdByRequestStore($requestStore); $this->updatePage('page100', 0, ['store_id' => $newStoreId]); - $page = $this->getPageByIdentifier->execute('page100', $newStoreId); + $page = $this->loadPageByIdentifier('page100', $newStoreId); $expectedData = array_intersect_key( $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), $this->getPageRequestData()['page'] @@ -257,7 +260,10 @@ public function testCreateByStores(string $requestStore): void $requestData = $this->getPageRequestData(); $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->createdPages[] = $this->loadPageByIdentifier( + $requestData['page'][PageInterface::IDENTIFIER], + $newStoreId + ); $this->assertResponseData($page, $requestData['page']); $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( @@ -370,7 +376,10 @@ public function testUpdateByStores(string $requestStore): void $requestData = $this->getPageRequestData(); $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->createdPages[] = $this->loadPageByIdentifier( + $requestData['page'][PageInterface::IDENTIFIER], + $newStoreId + ); $this->assertResponseData($page, $requestData['page']); $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( @@ -768,7 +777,7 @@ private function isPageInArray(array $pageGridData, int $pageId): bool */ private function updatePage(string $pageIdentifier, int $storeId, array $pageData): PageInterface { - $page = $this->getPageByIdentifier->execute($pageIdentifier, $storeId); + $page = $this->loadPageByIdentifier($pageIdentifier, $storeId); $page->addData($pageData); return $this->pageRepository->save($page); @@ -839,19 +848,17 @@ private function getPageGridDataByStoreCode(string $requestStore): array } /** - * Get page by identifier without throw exception + * Load page by identifier and store id * * @param string $identifier * @param int $storeId - * @return PageInterface|null + * @return PageInterface */ - private function getPageByIdentifier(string $identifier, int $storeId): ?PageInterface + private function loadPageByIdentifier(string $identifier, int $storeId): PageInterface { - $page = null; - try { - $page = $this->getPageByIdentifier->execute($identifier, $storeId); - } catch (NoSuchEntityException $exception) { - } + $page = $this->pageFactory->create(); + $page->setStoreId($storeId); + $this->pageResource->load($page, $identifier, PageInterface::IDENTIFIER); return $page; } From f047427d64d8b5f43e7ad9f6be25c8dc14e39167 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 5 Nov 2020 15:32:27 +0200 Subject: [PATCH 177/195] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Cms/Ui/Component/DataProviderTest.php | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php new file mode 100644 index 0000000000000..2a559566be786 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Ui\Component; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks Cms UI component data provider behaviour + * + * @magentoAppArea adminhtml + */ +class DataProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var UiComponentFactory */ + private $componentFactory; + + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->componentFactory = $this->objectManager->get(UiComponentFactory::class); + } + + /** + * @dataProvider pageFilterDataProvider + * + * @magentoDataFixture Magento/Cms/_files/pages.php + * + * @param array $filter + * @param string $expectedPage + * @return void + */ + public function testPageFiltering(array $filter, string $expectedPage): void + { + $this->request->setParams(['filters' => $filter]); + $data = $this->getComponentProvidedData('cms_page_listing'); + $this->assertCount(1, $data['items']); + $this->assertEquals(reset($data['items'])[PageInterface::IDENTIFIER], $expectedPage); + } + + /** + * @return array + */ + public function pageFilterDataProvider(): array + { + return [ + 'partial_title_filter' => [ + 'filter' => ['title' => 'Cms Page 1'], + 'expected_item' => 'page100', + ], + 'multiple_filter' => [ + 'filter' => [ + 'title' => 'Cms Page', + 'meta_title' => 'Cms Meta title for Blank page', + ], + 'expected_item' => 'page_design_blank', + ], + ]; + } + + /** + * @dataProvider blockFilterDataProvider + * + * @magentoDataFixture Magento/Cms/_files/blocks.php + * + * @return void + */ + public function testBlockFiltering(array $filter, string $expectedBlock): void + { + $this->request->setParams(['filters' => $filter]); + $data = $this->getComponentProvidedData('cms_block_listing'); + $this->assertCount(1, $data['items']); + $this->assertEquals(reset($data['items'])[BlockInterface::IDENTIFIER], $expectedBlock); + } + + /** + * @return array + */ + public function blockFilterDataProvider(): array + { + return [ + 'partial_title_filter' => [ + 'filter' => ['title' => 'Enabled CMS Block'], + 'expected_item' => 'enabled_block', + ], + 'multiple_filter' => [ + 'filter' => [ + 'title' => 'CMS Block Title', + 'is_active' => [0], + ], + 'expected_item' => 'disabled_block', + ], + ]; + } + + /** + * Call prepare method in the child components + * + * @param UiComponentInterface $component + * @return void + */ + private function prepareChildComponents(UiComponentInterface $component) + { + foreach ($component->getChildComponents() as $child) { + $this->prepareChildComponents($child); + } + + $component->prepare(); + } + + /** + * Get component provided data + * + * @param string $namespace + * @return array + */ + private function getComponentProvidedData(string $namespace): array + { + $component = $this->componentFactory->create($namespace); + $this->prepareChildComponents($component); + + return $component->getContext()->getDataProvider()->getData(); + } +} From aca349edd7af5732a27d4862bae937ffb9cbecfc Mon Sep 17 00:00:00 2001 From: rrego6 <rrego@adobe.com> Date: Thu, 5 Nov 2020 10:37:21 -0500 Subject: [PATCH 178/195] MC-24057: Implement static dependency analysis for wildcard and API urls - added id attributes back to ConfigReader --- .../static/testsuite/Magento/Test/Integrity/DependencyTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index ac6bf8a026917..2620c48659d98 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -292,6 +292,8 @@ protected static function _initRules() 'webapi.xml', [ '/routes/route' => ['url', 'method'], + '/routes/route/resources/resource' => 'ref', + '/routes/route/data/parameter' => 'name', ], ); From 079f111b87a139c90becc3f5a8c53a28f221685a Mon Sep 17 00:00:00 2001 From: Ji Lu <jilu1@adobe.com> Date: Thu, 5 Nov 2020 11:37:25 -0600 Subject: [PATCH 179/195] MQE-2366: composer.lock updade for mftf 3.2.0 release --- composer.lock | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index b06e0e9fa9e5c..898039b4c8fc5 100644 --- a/composer.lock +++ b/composer.lock @@ -7947,16 +7947,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.1", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "c6760313811f2c04545a261c706d2a73dd727b9a" + "reference": "0ec0c87335af996cbf3c0aace375d4e659e7a6dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/c6760313811f2c04545a261c706d2a73dd727b9a", - "reference": "c6760313811f2c04545a261c706d2a73dd727b9a", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/0ec0c87335af996cbf3c0aace375d4e659e7a6dc", + "reference": "0ec0c87335af996cbf3c0aace375d4e659e7a6dc", "shasum": "" }, "require": { @@ -8034,7 +8034,7 @@ "magento", "testing" ], - "time": "2020-09-28T18:26:59+00:00" + "time": "2020-11-05T15:57:52+00:00" }, { "name": "mikey179/vfsstream", @@ -11297,6 +11297,5 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } From 6a2a4e2844c8fcb87abf412eaad2619250f05b53 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 5 Nov 2020 20:55:23 +0200 Subject: [PATCH 180/195] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Cms/Ui/Component/DataProviderTest.php | 65 ++++--------------- 1 file changed, 11 insertions(+), 54 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php index 2a559566be786..3e7b1ed4d4c55 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -20,6 +20,7 @@ * Checks Cms UI component data provider behaviour * * @magentoAppArea adminhtml + * @magentoDbIsolation enabled */ class DataProviderTest extends TestCase { @@ -45,75 +46,31 @@ protected function setUp(): void } /** - * @dataProvider pageFilterDataProvider - * * @magentoDataFixture Magento/Cms/_files/pages.php * - * @param array $filter - * @param string $expectedPage * @return void */ - public function testPageFiltering(array $filter, string $expectedPage): void + public function testPageFilteringByTitlePart(): void { - $this->request->setParams(['filters' => $filter]); + $this->request->setParams(['search' => 'Cms Page 1']); $data = $this->getComponentProvidedData('cms_page_listing'); - $this->assertCount(1, $data['items']); - $this->assertEquals(reset($data['items'])[PageInterface::IDENTIFIER], $expectedPage); - } - - /** - * @return array - */ - public function pageFilterDataProvider(): array - { - return [ - 'partial_title_filter' => [ - 'filter' => ['title' => 'Cms Page 1'], - 'expected_item' => 'page100', - ], - 'multiple_filter' => [ - 'filter' => [ - 'title' => 'Cms Page', - 'meta_title' => 'Cms Meta title for Blank page', - ], - 'expected_item' => 'page_design_blank', - ], - ]; + $items = $data['items']; + $this->assertCount(1, $items); + $this->assertEquals('page100', reset($items)[PageInterface::IDENTIFIER]); } /** - * @dataProvider blockFilterDataProvider - * * @magentoDataFixture Magento/Cms/_files/blocks.php * * @return void */ - public function testBlockFiltering(array $filter, string $expectedBlock): void + public function testBlockFilteringByTitlePart(): void { - $this->request->setParams(['filters' => $filter]); + $this->request->setParams(['search' => 'Enabled CMS Block']); $data = $this->getComponentProvidedData('cms_block_listing'); - $this->assertCount(1, $data['items']); - $this->assertEquals(reset($data['items'])[BlockInterface::IDENTIFIER], $expectedBlock); - } - - /** - * @return array - */ - public function blockFilterDataProvider(): array - { - return [ - 'partial_title_filter' => [ - 'filter' => ['title' => 'Enabled CMS Block'], - 'expected_item' => 'enabled_block', - ], - 'multiple_filter' => [ - 'filter' => [ - 'title' => 'CMS Block Title', - 'is_active' => [0], - ], - 'expected_item' => 'disabled_block', - ], - ]; + $items = $data['items']; + $this->assertCount(1, $items); + $this->assertEquals('enabled_block', reset($items)[BlockInterface::IDENTIFIER]); } /** From 4b7bffac1f021e5b51f1b8d6bf783d9a332b6f2b Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Thu, 5 Nov 2020 18:36:29 -0600 Subject: [PATCH 181/195] MC-38613: Support by Magento CatalogGraphQl --- app/code/Magento/Eav/Model/AttributeRepository.php | 2 +- app/code/Magento/Swatches/Helper/Media.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Eav/Model/AttributeRepository.php b/app/code/Magento/Eav/Model/AttributeRepository.php index bb307d5581121..ee2db9a9b6b35 100644 --- a/app/code/Magento/Eav/Model/AttributeRepository.php +++ b/app/code/Magento/Eav/Model/AttributeRepository.php @@ -88,7 +88,7 @@ public function save(\Magento\Eav\Api\Data\AttributeInterface $attribute) try { $this->eavResource->save($attribute); } catch (\Exception $e) { - throw new StateException(__("The attribute can't be saved.")); + throw new StateException(__("The attribute can't be saved."), $e); } return $attribute; } diff --git a/app/code/Magento/Swatches/Helper/Media.php b/app/code/Magento/Swatches/Helper/Media.php index bfcb354b41dfb..6787fba534893 100644 --- a/app/code/Magento/Swatches/Helper/Media.php +++ b/app/code/Magento/Swatches/Helper/Media.php @@ -197,7 +197,7 @@ protected function getUniqueFileName($file) $file ); } else { - $destFile = dirname($file) . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( + $destFile = rtrim(dirname($file), '/.') . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( $this->getOriginalFilePath($file) ); } From d108146652dcb46db68d4262146730c18a9be0bd Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Thu, 5 Nov 2020 21:04:31 -0600 Subject: [PATCH 182/195] MC-38613: Support by Magento CatalogGraphQl --- .../visual_swatch_attribute_with_different_options_type.php | 2 +- .../Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index a4a755c4b92db..8d2b427d7f7f3 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -24,7 +24,7 @@ $imagesGenerator = Bootstrap::getObjectManager()->get(ImagesGenerator::class); /** @var SwatchesMedia $swatchesMedia */ $swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); -$imageName = 'visual_swatch_attribute_option_type_image.jpg'; +$imageName = '/visual_swatch_attribute_option_type_image.jpg'; $imagesGenerator->generate([ 'image-width' => 110, 'image-height' => 90, diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index bc6d57a869b5a..dc730b69f8775 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -71,6 +71,7 @@ public function generate($config) $mediaDirectory->create($relativePathToMedia); $imagePath = $relativePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; + $imagePath = preg_replace('|/{2,}|', '/', $imagePath); $memory = fopen('php://memory', 'r+'); if(!imagejpeg($image, $memory)) { throw new \Exception('Could not create picture ' . $imagePath); From d38d0575ce79c0802e0818b3c56d45139dfd9e15 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Thu, 5 Nov 2020 21:48:51 -0600 Subject: [PATCH 183/195] MC-38613: Support by Magento CatalogGraphQl --- .../visual_swatch_attribute_with_different_options_type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index 8d2b427d7f7f3..77b3e198bd5ab 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -30,7 +30,7 @@ 'image-height' => 90, 'image-name' => $imageName, ]); -$imagePath = substr($swatchesMedia->moveImageFromTmp($imageName), 1); +$imagePath = $swatchesMedia->moveImageFromTmp($imageName); $swatchesMedia->generateSwatchVariations($imagePath); // Add attribute data From ae9c0364d2c9c3887c893871ed66dbe33c0b8e63 Mon Sep 17 00:00:00 2001 From: Roman Hanin <rganin@adobe.com> Date: Fri, 6 Nov 2020 00:33:14 -0600 Subject: [PATCH 184/195] B2B-964: Create MFTF Test for Purchase Order Creation with Online Payment Methods --- ...ayPalExpressCheckoutDisableActionGroup.xml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml new file mode 100644 index 0000000000000..f70af75660f94 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.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="SampleConfigPayPalExpressCheckoutDisableActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + </actionGroup> +</actionGroups> From e8205458686ae20ee6159ced11a972070a08c96c Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Fri, 6 Nov 2020 08:38:41 +0200 Subject: [PATCH 185/195] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../Adminhtml/Order/Create/ReorderTest.php | 105 ++++++++++++++---- 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php index 0856e58c308d5..6390e5aeaba5f 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -7,61 +7,65 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Backend\Model\Session\Quote; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Framework\App\Request\Http; -use Magento\Framework\Registry; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\OrderFactory; -use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Xpath; -use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\Sales\Api\Data\OrderInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Exception\NoSuchEntityException; /** - * Test load block for order create controller. - * - * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Index + * Test for reorder controller. * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder * @magentoAppArea adminhtml - * @magentoDbIsolation enabled */ class ReorderTest extends AbstractBackendController { - /** - * @var OrderRepositoryInterface - */ - private $orderRepository; + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; /** - * @var CustomerInterfaceFactory + * @var CustomerRepositoryInterface */ - private $customerFactory; + private $customerRepository; /** - * @var AccountManagementInterface + * @var array */ - private $accountManagement; + private $customerIds = []; /** - * @var OrderFactory + * @var OrderRepositoryInterface */ - private $orderFactory; + private $orderRepository; /** - * @var CustomerRepositoryInterface + * @var CustomerInterfaceFactory */ - private $customerRepository; + private $customerFactory; /** - * @var array + * @var AccountManagementInterface */ - private $customerIds = []; + private $accountManagement; /** * @inheritdoc @@ -69,10 +73,11 @@ class ReorderTest extends AbstractBackendController protected function setUp(): void { parent::setUp(); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); $this->orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class); $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); - $this->orderFactory = $this->_objectManager->get(OrderFactory::class); $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); } @@ -81,7 +86,9 @@ protected function setUp(): void */ protected function tearDown(): void { - parent::tearDown(); + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } foreach ($this->customerIds as $customerId) { try { $this->customerRepository->deleteById($customerId); @@ -89,6 +96,24 @@ protected function tearDown(): void //customer already deleted } } + parent::tearDown(); + } + + /** + * Reorder with JS calendar options + * + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoDataFixture Magento/Sales/_files/order_with_date_time_option_product.php + * + * @return void + */ + public function testReorderAfterJSCalendarEnabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote('customer@example.com'); + $this->assertTrue(!empty($this->quote)); } /** @@ -150,4 +175,36 @@ private function getOrderWithDelegatingCustomer(): OrderInterface return $this->orderRepository->save($orderModel); } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/reorder'); + } + + /** + * Gets quote by reserved order id. + * + * @return \Magento\Quote\Api\Data\CartInterface + */ + private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('customer_email', $customerEmail) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } } From ee08b091b4cd38e30f63a937e75244dc97b783e5 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Fri, 6 Nov 2020 10:56:55 +0200 Subject: [PATCH 186/195] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../Sales/Controller/Adminhtml/Order/Create/ReorderTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php index 6390e5aeaba5f..27423c67ffe19 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -30,6 +30,7 @@ * * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ReorderTest extends AbstractBackendController { From 533a39d6061034a42d1989f9bb350ff0175baf76 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 6 Nov 2020 11:38:00 +0200 Subject: [PATCH 187/195] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../testsuite/Magento/Cms/Ui/Component/DataProviderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php index 3e7b1ed4d4c55..710c49241d82c 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -79,7 +79,7 @@ public function testBlockFilteringByTitlePart(): void * @param UiComponentInterface $component * @return void */ - private function prepareChildComponents(UiComponentInterface $component) + private function prepareChildComponents(UiComponentInterface $component): void { foreach ($component->getChildComponents() as $child) { $this->prepareChildComponents($child); From 40ebd2d2b99b85c5ded30bbb43e1ae3dfc36e1b3 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 6 Nov 2020 12:12:54 +0200 Subject: [PATCH 188/195] MC-37074: Create automated test for "Dynamic charts on Magento dashboard --- .../Block/Dashboard/Chart/PeriodTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php new file mode 100644 index 0000000000000..c9ad4827c2838 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Block\Dashboard\Chart; + +use Magento\Backend\ViewModel\ChartsPeriod; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Checks chart periods on Magento dashboard + * + * @magentoAppArea adminhtml + */ +class PeriodTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Template */ + private $block; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Template::class); + $this->block->setTemplate("Magento_Backend::dashboard/chart/period.phtml"); + $this->block->setData('view_model', $this->objectManager->get(ChartsPeriod::class)); + } + + /** + * @return void + */ + public function testChartPeriodOptions(): void + { + $html = $this->block->toHtml(); + $dropDownList = [ + __('Last 24 Hours'), + __('Last 7 Days'), + __('Current Month'), + __('YTD'), + __('2YTD') + ]; + foreach ($dropDownList as $item) { + $xPath = "//select[@id='dashboard_chart_period']/option[normalize-space(text())='{$item}']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath($xPath, $html)); + } + } +} From d64fe798a69adf933eb28fe2fdde8ad4d6b2bf9e Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Fri, 6 Nov 2020 09:57:02 -0600 Subject: [PATCH 189/195] B2B-964: Create MFTF Test for Purchase Order Creation with Online Payment Methods - Addressing PR feedback --- ... => AdminConfigPayPalExpressCheckoutDisableActionGroup.xml} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename app/code/Magento/Paypal/Test/Mftf/ActionGroup/{SampleConfigPayPalExpressCheckoutDisableActionGroup.xml => AdminConfigPayPalExpressCheckoutDisableActionGroup.xml} (91%) diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml similarity index 91% rename from app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml rename to app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml index f70af75660f94..42dbf1d1c6061 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SampleConfigPayPalExpressCheckoutDisableActionGroup"> + <actionGroup name="AdminConfigPayPalExpressCheckoutDisableActionGroup"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> </annotations> @@ -22,5 +22,6 @@ <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> </actionGroups> From 899fa5fb2458558967e324f51d8694c72dde614f Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Tue, 3 Nov 2020 00:38:08 -0600 Subject: [PATCH 190/195] MC-38242: Qty increments wrong calculation - Change the validation of the remainder to get the exact result --- ...AdminSetEnableQtyIncrementsActionGroup.xml | 23 +++++++ ...nSetQtyIncrementsForProductActionGroup.xml | 23 +++++++ ...tityIncrementsWithDecimalInventoryTest.xml | 66 +++++++++++++++++++ lib/web/mage/validation.js | 14 +++- 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml create mode 100644 app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml new file mode 100644 index 0000000000000..2e211dad6dc81 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetEnableQtyIncrementsActionGroup"> + <annotations> + <description>Set "Enable Qty Increments" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="scrollToEnableQtyIncrements"/> + <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="clickOnEnableQtyIncrementsUseConfigSettingsCheckbox"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" userInput="{{value}}" + stepKey="setEnableQtyIncrements"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml new file mode 100644 index 0000000000000..6ea82a2f2a490 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetQtyIncrementsForProductActionGroup"> + <annotations> + <description>Fills in the "Qty Increments" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="scrollToQtyIncrementsUseConfigSettings"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="clickOnQtyIncrementsUseConfigSettings"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" stepKey="scrollToQtyIncrements"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput="{{qty}}" stepKey="fillQtyIncrements"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml new file mode 100644 index 0000000000000..e17c8fe65d4cf --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml @@ -0,0 +1,66 @@ +<?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="StorefrontValidateQuantityIncrementsWithDecimalInventoryTest"> + <annotations> + <features value="CatalogInventory"/> + <stories value="Qty increments wrong calculation for decimal fraction quantity"/> + <title value="Validate qty increments for decimal fraction quantity works"/> + <description value="Validate qty increments for decimal fraction quantity works"/> + <severity value="MAJOR"/> + <useCaseId value="MC-38242"/> + <testCaseId value="MC-38883"/> + <group value="catalogInventory"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <!--Clear Filters--> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct" stepKey="deletePreReqSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Step1. Login as admin. Go to Catalog > Products page. Filtering *prod1*. Open *prod1* to edit--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin" /> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Step2. Update product Advanced Inventory Setting. + Set *Qty Uses Decimals* to *Yes* and *Enable Qty Increments* to *Yes* and *Qty Increments* to *3.33*. --> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetEnableQtyIncrementsActionGroup" stepKey="setEnableQtyIncrements"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetQtyIncrementsForProductActionGroup" stepKey="setQtyIncrementsValue"> + <argument name="qty" value="3.33"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + + <!--Step3. Save the product--> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton2"/> + <!--Step4. Open *Customer view* (Go to *Store Front*). Open *prod1* page (Find via search and click on product name) --> + <amOnPage url="{{StorefrontHomePage.url}}$$createPreReqSimpleProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> + <!--Step5. Fill *23.31* in *Qty*. Click on button *Add to Cart*--> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="23.31" stepKey="fillQty"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="clickOnAddToCart"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> + <!--Step6. Verify the product is successfully added to the cart with success message--> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createPreReqSimpleProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + </test> +</tests> diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index de40e3afa40ab..ae8dad5865709 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -204,12 +204,24 @@ define([ * @returns {float} */ function resolveModulo(qty, qtyIncrements) { + var divideEpsilon = 10000, + epsilon, + remainder; + while (qtyIncrements < 1) { qty *= 10; qtyIncrements *= 10; } - return qty % qtyIncrements; + epsilon = qtyIncrements / divideEpsilon; + remainder = qty % qtyIncrements; + + if (Math.abs(remainder - qtyIncrements) < epsilon || + Math.abs(remainder) < epsilon) { + remainder = 0; + } + + return remainder; } /** From d3bd1ffbf8bec185c19047cd3c01654b6eb9f260 Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Fri, 6 Nov 2020 10:05:22 -0600 Subject: [PATCH 191/195] B2B-964: Create MFTF Test for Purchase Order Creation with Online Payment Methods - Deprecating old PayPal actiongroup --- ...yPalExpressCheckoutDisableActionGroup.xml} | 3 +- ...PayPalExpressCheckoutEnableActionGroup.xml | 34 +++++++++++++++++++ ...ConfigPayPalExpressCheckoutActionGroup.xml | 2 +- ...ResolutionForPayPalInUnitedKingdomTest.xml | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) rename app/code/Magento/Paypal/Test/Mftf/ActionGroup/{AdminConfigPayPalExpressCheckoutDisableActionGroup.xml => AdminPayPalExpressCheckoutDisableActionGroup.xml} (92%) create mode 100644 app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml similarity index 92% rename from app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml rename to app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml index 42dbf1d1c6061..c927bfc50120e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminConfigPayPalExpressCheckoutDisableActionGroup"> + <actionGroup name="AdminPayPalExpressCheckoutDisableActionGroup"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> </annotations> @@ -20,7 +20,6 @@ <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> - <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml new file mode 100644 index 0000000000000..b6b44abd7b794 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml @@ -0,0 +1,34 @@ +<?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="AdminPayPalExpressCheckoutEnableActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalExpressConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.email(countryCode)}}" userInput="{{credentials.paypal_express_email}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod(countryCode)}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.username(countryCode)}}" userInput="{{credentials.paypal_express_api_username}}" stepKey="inputAPIUsername"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.password(countryCode)}}" userInput="{{credentials.paypal_express_api_password}}" stepKey="inputAPIPassword"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.paypal_express_api_signature}}" stepKey="inputAPISignature"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.paypal_express_merchantID}}" stepKey="inputMerchantID"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml index 23d956c8e9b8f..a7ccf0a19263b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SampleConfigPayPalExpressCheckoutActionGroup"> + <actionGroup name="SampleConfigPayPalExpressCheckoutActionGroup" deprecated="Use AdminPayPalExpressCheckoutEnableActionGroup instead"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> </annotations> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml index ebdfb9e91ecf1..a616c0bb2c68b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="SampleConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpress"> + <actionGroup ref="AdminPayPalExpressCheckoutEnableActionGroup" stepKey="ConfigPayPalExpress"> <argument name="credentials" value="SamplePaypalExpressConfig"/> </actionGroup> </before> From 8cc84806fb77eee669bd65265df750a8823966c4 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Wed, 28 Oct 2020 19:38:21 -0500 Subject: [PATCH 192/195] MC-38719: Adding Tier price - Issues with Magento Admin Panel Currency Display - Adding javascript component and helper class - Renaming files --- .../Modifier/CurrencySymbolProviderTest.php | 304 ++++++++++++++++++ .../Product/Form/Modifier/AdvancedPricing.php | 27 +- .../Form/Modifier/CurrencySymbolProvider.php | 139 ++++++++ .../Product/Form/Modifier/TierPrice.php | 4 + .../js/components/website-currency-symbol.js | 30 ++ 5 files changed, 498 insertions(+), 6 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php create mode 100644 app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php new file mode 100644 index 0000000000000..07b3de40c31f8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php @@ -0,0 +1,304 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CurrencySymbolProvider; +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product; +use Magento\Directory\Model\Currency as CurrencyModel; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Zend_Currency; + +/** + * Test class for Website Currency Symbol provider + */ +class CurrencySymbolProviderTest extends TestCase +{ + /** + * @var CurrencySymbolProvider|MockObject + */ + private $currencySymbolProvider; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var LocatorInterface|MockObject + */ + private $locatorMock; + + /** + * @var CurrencyInterface|MockObject + */ + private $localeCurrencyMock; + + /** + * @var StoreInterface|MockObject + */ + private $currentStoreMock; + + /** + * @var CurrencyModel|MockObject + */ + private $currencyMock; + + /** + * @var Zend_Currency|MockObject + */ + private $websiteCurrencyMock; + + /** + * @var Product|MockObject + */ + private $productMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->scopeConfigMock = $this->getMockForAbstractClass( + ScopeConfigInterface::class, + [], + '', + true, + true, + true, + ['getValue'] + ); + $this->storeManagerMock = $this->getMockForAbstractClass( + StoreManagerInterface::class, + [], + '', + true, + true, + true, + ['getWebsites'] + ); + $this->currentStoreMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + true, + true, + true, + ['getBaseCurrency'] + ); + $this->currencyMock = $this->createMock(CurrencyModel::class); + $this->websiteCurrencyMock = $this->createMock(Zend_Currency::class); + $this->productMock = $this->createMock(Product::class); + $this->locatorMock = $this->getMockForAbstractClass( + LocatorInterface::class, + [], + '', + true, + true, + true, + ['getStore', 'getProduct'] + ); + $this->localeCurrencyMock = $this->getMockForAbstractClass( + CurrencyInterface::class, + [], + '', + true, + true, + true, + ['getWebsites', 'getCurrency'] + ); + $this->currencySymbolProvider = $objectManager->getObject( + CurrencySymbolProvider::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'storeManager' => $this->storeManagerMock, + 'locator' => $this->locatorMock, + 'localeCurrency' => $this->localeCurrencyMock + ] + ); + } + + /** + * Test for Get option array of currency symbol prefixes. + * + * @param int $catalogPriceScope + * @param string $defaultStoreCurrencySymbol + * @param array $listOfWebsites + * @param array $productWebsiteIds + * @param array $currencySymbols + * @param array $actualResult + * @dataProvider getWebsiteCurrencySymbolDataProvider + */ + public function testGetCurrenciesPerWebsite( + int $catalogPriceScope, + string $defaultStoreCurrencySymbol, + array $listOfWebsites, + array $productWebsiteIds, + array $currencySymbols, + array $actualResult + ): void { + $this->locatorMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->currentStoreMock); + $this->currentStoreMock->expects($this->any()) + ->method('getBaseCurrency') + ->willReturn($this->currencyMock); + $this->currencyMock->expects($this->any()) + ->method('getCurrencySymbol') + ->willReturn($defaultStoreCurrencySymbol); + $this->scopeConfigMock + ->expects($this->any()) + ->method('getValue') + ->willReturn($catalogPriceScope); + $this->locatorMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->storeManagerMock->expects($this->any()) + ->method('getWebsites') + ->willReturn($listOfWebsites); + $this->productMock->expects($this->any()) + ->method('getWebsiteIds') + ->willReturn($productWebsiteIds); + $this->localeCurrencyMock->expects($this->any()) + ->method('getCurrency') + ->willReturn($this->websiteCurrencyMock); + foreach ($currencySymbols as $currencySymbol) { + $this->websiteCurrencyMock->expects($this->any()) + ->method('getSymbol') + ->willReturn($currencySymbol); + } + $expectedResult = $this->currencySymbolProvider + ->getCurrenciesPerWebsite(); + $this->assertEquals($expectedResult, $actualResult); + } + + /** + * DataProvider for getCurrenciesPerWebsite. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getWebsiteCurrencySymbolDataProvider(): array + { + return [ + 'verify website currency with default website and global price scope' => [ + 'catalogPriceScope' => 0, + 'defaultStoreCurrencySymbol' => '$', + 'listOfWebsites' => $this->getWebsitesMock( + [ + [ + 'id' => '1', + 'name' => 'Main Website', + 'code' => 'main_website', + 'base_currency_code' => 'USD', + 'currency_symbol' => '$' + ] + ] + ), + 'productWebsiteIds' => ['1'], + 'currencySymbols' => ['$'], + 'actualResult' => ['$'] + ], + 'verify website currency with default website and website price scope' => [ + 'catalogPriceScope' => 1, + 'defaultStoreCurrencySymbol' => '$', + 'listOfWebsites' => $this->getWebsitesMock( + [ + [ + 'id' => '1', + 'name' => 'Main Website', + 'code' => 'main_website', + 'base_currency_code' => 'USD', + 'currency_symbol' => '$' + ] + ] + ), + 'productWebsiteIds' => ['1'], + 'currencySymbols' => ['$'], + 'actualResult' => ['$', '$'] + ], + 'verify website currency with two website and website price scope' => [ + 'catalogPriceScope' => 1, + 'defaultStoreCurrencySymbol' => '$', + 'listOfWebsites' => $this->getWebsitesMock( + [ + [ + 'id' => '1', + 'name' => 'Main Website', + 'code' => 'main_website', + 'base_currency_code' => 'USD', + 'currency_symbol' => '$' + ], + [ + 'id' => '2', + 'name' => 'Indian Website', + 'code' => 'indian_website', + 'base_currency_code' => 'INR', + 'currency_symbol' => '₹' + ] + ] + ), + 'productWebsiteIds' => ['1', '2'], + 'currencySymbols' => ['$', '₹'], + 'actualResult' => ['$', '$', '$'] + ] + ]; + } + + /** + * Get list of websites mock + * + * @param array $websites + * @return array + */ + private function getWebsitesMock(array $websites): array + { + $websitesMock = []; + foreach ($websites as $key => $website) { + $websitesMock[$key] = $this->getMockForAbstractClass( + WebsiteInterface::class, + [], + '', + true, + true, + true, + ['getId', 'getBaseCurrencyCode'] + ); + $websitesMock[$key]->expects($this->any()) + ->method('getId') + ->willReturn($website['id']); + $websitesMock[$key]->expects($this->any()) + ->method('getBaseCurrencyCode') + ->willReturn($website['base_currency_code']); + } + return $websitesMock; + } + + protected function tearDown(): void + { + unset($this->scopeConfigMock); + unset($this->storeManagerMock); + unset($this->currentStoreMock); + unset($this->currencyMock); + unset($this->websiteCurrencyMock); + unset($this->productMock); + unset($this->locatorMock); + unset($this->localeCurrencyMock); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 174a01b72a109..8c9421b073394 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -100,6 +100,11 @@ class AdvancedPricing extends AbstractModifier */ private $customerGroupSource; + /** + * @var CurrencySymbolProvider + */ + private $currencySymbolProvider; + /** * @param LocatorInterface $locator * @param StoreManagerInterface $storeManager @@ -110,7 +115,8 @@ class AdvancedPricing extends AbstractModifier * @param Data $directoryHelper * @param ArrayManager $arrayManager * @param string $scopeName - * @param GroupSourceInterface $customerGroupSource + * @param GroupSourceInterface|null $customerGroupSource + * @param CurrencySymbolProvider|null $currencySymbolProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -123,7 +129,8 @@ public function __construct( Data $directoryHelper, ArrayManager $arrayManager, $scopeName = '', - GroupSourceInterface $customerGroupSource = null + GroupSourceInterface $customerGroupSource = null, + ?CurrencySymbolProvider $currencySymbolProvider = null ) { $this->locator = $locator; $this->storeManager = $storeManager; @@ -136,6 +143,8 @@ public function __construct( $this->scopeName = $scopeName; $this->customerGroupSource = $customerGroupSource ?: ObjectManager::getInstance()->get(GroupSourceInterface::class); + $this->currencySymbolProvider = $currencySymbolProvider + ?: ObjectManager::getInstance()->get(CurrencySymbolProvider::class); } /** @@ -488,6 +497,7 @@ private function getTierPriceStructure($tierPricePath) 'arguments' => [ 'data' => [ 'config' => [ + 'component' => 'Magento_Catalog/js/components/website-currency-symbol', 'dataType' => Text::NAME, 'formElement' => Select::NAME, 'componentType' => Field::NAME, @@ -498,6 +508,10 @@ private function getTierPriceStructure($tierPricePath) 'visible' => $this->isMultiWebsites(), 'disabled' => ($this->isShowWebsiteColumn() && !$this->isAllowChangeWebsite()), 'sortOrder' => 10, + 'currenciesForWebsites' => $this->currencySymbolProvider + ->getCurrenciesPerWebsite(), + 'currency' => $this->currencySymbolProvider + ->getDefaultCurrency(), ], ], ], @@ -548,9 +562,6 @@ private function getTierPriceStructure($tierPricePath) 'label' => __('Price'), 'enableLabel' => true, 'dataScope' => 'price', - 'addbefore' => $this->locator->getStore() - ->getBaseCurrency() - ->getCurrencySymbol(), 'sortOrder' => 40, 'validation' => [ 'required-entry' => true, @@ -559,8 +570,12 @@ private function getTierPriceStructure($tierPricePath) ], 'imports' => [ 'priceValue' => '${ $.provider }:data.product.price', - '__disableTmpl' => ['priceValue' => false], + '__disableTmpl' => ['priceValue' => false, 'addbefore' => false], + 'addbefore' => '${ $.parentName }:currency' ], + 'tracks' => [ + 'addbefore' => true + ] ], ], ], diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php new file mode 100644 index 0000000000000..b46ca682e576a --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Website; + +/** + * Website Currency Symbol provider + */ +class CurrencySymbolProvider +{ + /** + * Scope Config Details + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Store Information + * + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Store locator + * + * @var LocatorInterface + */ + private $locator; + + /** + * Locale Currency + * + * @var CurrencyInterface + */ + private $localeCurrency; + + /** + * Initialize objects for website currency scope + * + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param LocatorInterface $locator + * @param CurrencyInterface $localeCurrency + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + LocatorInterface $locator, + CurrencyInterface $localeCurrency + ) { + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; + $this->locator = $locator; + $this->localeCurrency = $localeCurrency; + } + + /** + * Get option array of currency symbol prefixes. + * + * @return array + */ + public function getCurrenciesPerWebsite(): array + { + $baseCurrency = $this->locator->getStore() + ->getBaseCurrency(); + $websitesCurrencySymbol[0] = $baseCurrency->getCurrencySymbol() ?? + $baseCurrency->getCurrencyCode(); + $catalogPriceScope = $this->getCatalogPriceScope(); + $product = $this->locator->getProduct(); + $websitesList = $this->storeManager->getWebsites(); + $productWebsiteIds = $product->getWebsiteIds(); + if ($catalogPriceScope!=0) { + foreach ($websitesList as $website) { + /** @var Website $website */ + if (!in_array($website->getId(), $productWebsiteIds)) { + continue; + } + $websitesCurrencySymbol[$website->getId()] = $this + ->getCurrencySymbol( + $website->getBaseCurrencyCode() + ); + } + } + return $websitesCurrencySymbol; + } + + /** + * Get default store currency symbol + * + * @return string + */ + public function getDefaultCurrency(): string + { + $baseCurrency = $this->locator->getStore() + ->getBaseCurrency(); + return $baseCurrency->getCurrencySymbol() ?? + $baseCurrency->getCurrencyCode(); + } + + /** + * Get catalog price scope from the admin config + * + * @return int + */ + public function getCatalogPriceScope(): int + { + return (int) $this->scopeConfig->getValue( + Store::XML_PATH_PRICE_SCOPE, + ScopeInterface::SCOPE_WEBSITE + ); + } + + /** + * Retrieve currency name by code + * + * @param string $code + * @return string + */ + private function getCurrencySymbol(string $code): string + { + $currency = $this->localeCurrency->getCurrency($code); + return $currency->getSymbol() ? + $currency->getSymbol() : $currency->getShortName(); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index e9e8229e581ba..25e04302bd33c 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -118,6 +118,10 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'showLabel' => false, 'dataScope' => '', 'additionalClasses' => 'control-grouped', + 'imports' => [ + 'currency' => '${ $.parentName }.website_id:currency', + '__disableTmpl' => ['currency' => false], + ], 'sortOrder' => isset($priceMeta['arguments']['data']['config']['sortOrder']) ? $priceMeta['arguments']['data']['config']['sortOrder'] : 40, ], diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js new file mode 100644 index 0000000000000..069bd9baed86f --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/select' +], function (Select) { + 'use strict'; + + return Select.extend({ + defaults: { + currenciesForWebsites: {}, + tracks: { + currency: true + } + }, + + /** + * Set currency symbol per website + * + * @param {String} value - currency symbol + */ + setDifferedFromDefault: function (value) { + this.currency = this.currenciesForWebsites[value]; + + return this._super(); + } + }); +}); From d39430474e7a1f51f5f9f4071033df6bfac89e67 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Tue, 27 Oct 2020 03:28:18 -0500 Subject: [PATCH 193/195] MC-38666: "total_qty" in invoice does not match "total_qty_ordered" from sales_order - Adding configurable product plugin --- .../UpdateConfigurableProductTotalQty.php | 43 +++++ .../UpdateConfigurableProductTotalQtyTest.php | 182 ++++++++++++++++++ .../ConfigurableProduct/etc/adminhtml/di.xml | 3 + 3 files changed, 228 insertions(+) create mode 100644 app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php create mode 100644 app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php new file mode 100644 index 0000000000000..28237ca71b07a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Model\Order\Invoice; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Sales\Model\Order\Invoice; + +/** + * Update total quantity for configurable product invoice + */ +class UpdateConfigurableProductTotalQty +{ + /** + * Set total quantity for configurable product invoice + * + * @param Invoice $invoice + * @param float $totalQty + * @return float + */ + public function beforeSetTotalQty( + Invoice $invoice, + float $totalQty + ): float { + $order = $invoice->getOrder(); + $productTotalQty = 0; + $hasConfigurableProduct = false; + foreach ($order->getAllItems() as $orderItem) { + if ($orderItem->getParentItemId() === null && + $orderItem->getProductType() == Configurable::TYPE_CODE + ) { + $hasConfigurableProduct = true; + continue; + } + $productTotalQty += (float) $orderItem->getQtyOrdered(); + } + return $hasConfigurableProduct ? $productTotalQty : $totalQty; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php new file mode 100644 index 0000000000000..bff629fd94ac2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php @@ -0,0 +1,182 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\Order\Invoice; + +use Magento\Bundle\Model\Product\Type as Bundle; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\Model\Order\Invoice\UpdateConfigurableProductTotalQty; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Item; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for class UpdateConfigurableProductTotalQty. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UpdateConfigurableProductTotalQtyTest extends TestCase +{ + /** + * @var UpdateConfigurableProductTotalQty + */ + private $model; + + /** + * @var ObjectManagerHelper|null + */ + private $objectManagerHelper; + + /** + * @var Invoice|MockObject + */ + private $invoiceMock; + + /** + * @var Order|MockObject + */ + private $orderMock; + + /** + * @var Item[]|MockObject + */ + private $orderItemsMock; + + protected function setUp(): void + { + $this->invoiceMock = $this->createMock(Invoice::class); + $this->orderMock = $this->createMock(Order::class); + $this->orderItemsMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + UpdateConfigurableProductTotalQty::class, + [] + ); + } + + /** + * Test Set total quantity for configurable product invoice + * + * @param array $orderItems + * @param float $totalQty + * @param float $productTotalQty + * @dataProvider getOrdersForConfigurableProducts + */ + public function testBeforeSetTotalQty( + array $orderItems, + float $totalQty, + float $productTotalQty + ): void { + $this->invoiceMock->expects($this->any()) + ->method('getOrder') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('getAllItems') + ->willReturn($orderItems); + $expectedQty= $this->model->beforeSetTotalQty($this->invoiceMock, $totalQty); + $this->assertEquals($expectedQty, $productTotalQty); + } + + /** + * DataProvider for beforeSetTotalQty. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getOrdersForConfigurableProducts(): array + { + + return [ + 'verify productQty for simple products' => [ + 'orderItems' => $this->getOrderItems( + [ + [ + 'parent_item_id' => null, + 'product_type' => 'simple', + 'qty_ordered' => 10 + ] + ] + ), + 'totalQty' => 10.00, + 'productTotalQty' => 10.00 + ], + 'verify productQty for configurable products' => [ + 'orderItems' => $this->getOrderItems( + [ + [ + 'parent_item_id' => '2', + 'product_type' => Configurable::TYPE_CODE, + 'qty_ordered' => 10 + ] + ] + ), + 'totalQty' => 10.00, + 'productTotalQty' => 10.00 + ], + 'verify productQty for simple configurable products' => [ + 'orderItems' => $this->getOrderItems( + [ + [ + 'parent_item_id' => null, + 'product_type' => 'simple', + 'qty_ordered' => 10 + ], + [ + 'parent_item_id' => '2', + 'product_type' => Configurable::TYPE_CODE, + 'qty_ordered' => 10 + ], + [ + 'parent_item_id' => '2', + 'product_type' => Bundle::TYPE_CODE, + 'qty_ordered' => 10 + ] + ] + ), + 'totalQty' => 30.00, + 'productTotalQty' => 30.00 + ] + ]; + } + + /** + * Get Order Items. + * + * @param array $orderItems + * @return array + */ + public function getOrderItems(array $orderItems): array + { + $orderItemsMock = []; + foreach ($orderItems as $key => $orderItem) { + $orderItemsMock[$key] = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->getMock(); + $orderItemsMock[$key]->expects($this->any()) + ->method('getParentItemId') + ->willReturn($orderItem['parent_item_id']); + $orderItemsMock[$key]->expects($this->any()) + ->method('getProductType') + ->willReturn($orderItem['product_type']); + $orderItemsMock[$key]->expects($this->any()) + ->method('getQtyOrdered') + ->willReturn($orderItem['qty_ordered']); + } + return $orderItemsMock; + } + + protected function tearDown(): void + { + unset($this->invoiceMock); + unset($this->orderMock); + unset($this->orderItemsMock); + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml index de6765138fce6..60ad9e03fc17e 100644 --- a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml @@ -78,4 +78,7 @@ </argument> </arguments> </virtualType> + <type name="Magento\Sales\Model\Order\Invoice"> + <plugin name="update_configurable_product_total_qty" type="Magento\ConfigurableProduct\Plugin\Model\Order\Invoice\UpdateConfigurableProductTotalQty"/> + </type> </config> From 1ab716627dbe07ce5484ac03524e4bc008838faf Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oleg.posyniak@gmail.com> Date: Mon, 9 Nov 2020 11:58:23 -0600 Subject: [PATCH 194/195] MC-38112: Add synchronization mechanism between local storage and remote storage (#357) --- app/code/Magento/AwsS3/Driver/AwsS3.php | 62 ++++---- .../Magento/AwsS3/etc/adminhtml/system.xml | 35 ----- .../Magento/Backup/Model/Fs/Collection.php | 5 - .../Reader/Source/Deployed/DocumentRoot.php | 20 +-- .../MediaGalleryRenditions/etc/config.xml | 5 + .../Model/CreateAssetFromFile.php | 13 +- .../MediaStorage/Model/File/Storage.php | 9 ++ .../Command/RemoteStorageDisableCommand.php | 74 --------- .../Command/RemoteStorageEnableCommand.php | 144 ------------------ .../RemoteStorageSynchronizeCommand.php | 83 ++++++++++ .../RemoteStorage/Driver/DriverPool.php | 21 ++- app/code/Magento/RemoteStorage/Filesystem.php | 10 +- .../RemoteStorage/FilesystemInterface.php | 19 +++ .../RemoteStorage/Model/Synchronizer.php | 104 +++++++++++++ .../Test/Unit/Model/SynchronizerTest.php | 109 +++++++++++++ .../_files/test/.dot_directory/child_file.txt | 0 .../Test/Unit/Model/_files/test/.dot_file.txt | 0 .../Test/Unit/Model/_files/test/root_file.txt | 0 app/code/Magento/RemoteStorage/etc/di.xml | 9 +- app/etc/di.xml | 1 - .../Magento/Framework/Config/DocumentRoot.php | 50 ------ .../Magento/Framework/File/Uploader.php | 27 +--- .../Filesystem/Directory/ReadFactory.php | 7 +- .../Filesystem/Directory/WriteFactory.php | 7 +- .../Framework/Filesystem/Driver/File.php | 27 +--- .../Framework/Filesystem/DriverPool.php | 2 +- .../Filesystem/DriverPoolInterface.php | 4 +- .../Filesystem/ExtendedDriverInterface.php | 11 +- .../Framework/Filesystem/File/ReadFactory.php | 8 +- .../Filesystem/File/WriteFactory.php | 5 +- 30 files changed, 423 insertions(+), 448 deletions(-) delete mode 100644 app/code/Magento/AwsS3/etc/adminhtml/system.xml delete mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php delete mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php create mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php create mode 100644 app/code/Magento/RemoteStorage/FilesystemInterface.php create mode 100644 app/code/Magento/RemoteStorage/Model/Synchronizer.php create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt delete mode 100644 lib/internal/Magento/Framework/Config/DocumentRoot.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 8b0469862a4e9..dcf52b3188404 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -8,6 +8,7 @@ namespace Magento\AwsS3\Driver; use Exception; +use Generator; use League\Flysystem\AdapterInterface; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; @@ -165,7 +166,7 @@ private function createDirectoryRecursively(string $path): bool $this->createDirectoryRecursively($parentDir); } - return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config([])); + return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config(self::CONFIG)); } /** @@ -205,8 +206,16 @@ public function deleteDirectory($path): bool public function filePutContents($path, $content, $mode = null): int { $path = $this->normalizeRelativePath($path); + $config = self::CONFIG; - return $this->adapter->write($path, $content, new Config(self::CONFIG))['size']; + if (false !== ($imageSize = @getimagesizefromstring($content))) { + $config['Metadata'] = [ + 'image-width' => $imageSize[0], + 'image-height' => $imageSize[1] + ]; + } + + return $this->adapter->write($path, $content, new Config($config))['size']; } /** @@ -461,16 +470,6 @@ public function getMetadata(string $path): array throw new FileSystemException(__('Cannot gather meta info! %1', [$this->getWarningMessage()])); } - $extra = [ - 'image-width' => 0, - 'image-height' => 0 - ]; - - if (isset($metaInfo['image-width'], $metaInfo['image-height'])) { - $extra['image-width'] = $metaInfo['image-width']; - $extra['image-height'] = $metaInfo['image-height']; - } - return [ 'path' => $metaInfo['path'], 'dirname' => $metaInfo['dirname'], @@ -480,7 +479,10 @@ public function getMetadata(string $path): array 'timestamp' => $metaInfo['timestamp'], 'size' => $metaInfo['size'], 'mimetype' => $metaInfo['mimetype'], - 'extra' => $extra + 'extra' => [ + 'image-width' => $metaInfo['metadata']['image-width'] ?? 0, + 'image-height' => $metaInfo['metadata']['image-height'] ?? 0 + ] ]; } @@ -489,21 +491,23 @@ public function getMetadata(string $path): array */ public function search($pattern, $path): array { - return $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')); + return iterator_to_array( + $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')), + false + ); } /** * Emulate php glob function for AWS S3 storage * * @param string $pattern - * @return array + * @return Generator * @throws FileSystemException */ - private function glob(string $pattern): array + private function glob(string $pattern): Generator { - $directoryContent = []; - $patternFound = preg_match('(\*|\?|\[.+\])', $pattern, $parentPattern, PREG_OFFSET_CAPTURE); + if ($patternFound) { // phpcs:ignore Magento2.Functions.DiscouragedFunction $parentDirectory = \dirname(substr($pattern, 0, $parentPattern[0][1] + 1)); @@ -512,13 +516,11 @@ private function glob(string $pattern): array $searchPattern = $this->getSearchPattern($pattern, $parentPattern, $parentDirectory, $index); if ($this->isDirectory($parentDirectory . '/')) { - $directoryContent = $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); + yield from $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); } } elseif ($this->isDirectory($pattern) || $this->isFile($pattern)) { - $directoryContent[] = $pattern; + yield $pattern; } - - return $directoryContent; } /** @@ -526,7 +528,7 @@ private function glob(string $pattern): array */ public function symlink($source, $destination, DriverInterface $targetDriver = null): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return $this->copy($source, $destination, $targetDriver); } /** @@ -850,7 +852,7 @@ private function getSearchPattern(string $pattern, array $parentPattern, string * @param string $searchPattern * @param string $leftover * @param int|bool $index - * @return array + * @return Generator * @throws FileSystemException */ private function getDirectoryContent( @@ -858,7 +860,7 @@ private function getDirectoryContent( string $searchPattern, string $leftover, $index - ): array { + ): Generator { $items = $this->readDirectory($parentDirectory . '/'); $directoryContent = []; foreach ($items as $item) { @@ -866,15 +868,9 @@ private function getDirectoryContent( // phpcs:ignore Magento2.Functions.DiscouragedFunction && strpos(basename($item), '.') !== 0) { if ($index === false || \strlen($leftover) === $index + 1) { - $directoryContent[] = $this->isDirectory($item) - ? rtrim($item, '/') . '/' - : $item; + yield $this->isDirectory($item) ? rtrim($item, '/') . '/' : $item; } elseif (strlen($leftover) > $index + 1) { - // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $directoryContent = array_merge( - $directoryContent, - $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)) - ); + yield from $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)); } } } diff --git a/app/code/Magento/AwsS3/etc/adminhtml/system.xml b/app/code/Magento/AwsS3/etc/adminhtml/system.xml deleted file mode 100644 index 0f97b96107ed3..0000000000000 --- a/app/code/Magento/AwsS3/etc/adminhtml/system.xml +++ /dev/null @@ -1,35 +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:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="system"> - <group id="file_system"> - <field id="access_key" translate="label comment" type="password" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Access Key</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - <field id="secret_key" translate="label comment" type="password" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Secret Key</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - <field id="bucket" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Bucket</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - <field id="region" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Region</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index 6102a63ec2f69..41a497495f687 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -40,11 +40,6 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem */ protected $_backup = null; - /** - * @var \Magento\Framework\Filesystem - */ - protected $_filesystem; - /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Magento\Backup\Helper\Data $backupData diff --git a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php index 2e50bbb8ef3c9..fb78de35569ac 100644 --- a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +++ b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php @@ -6,8 +6,7 @@ namespace Magento\Config\Model\Config\Reader\Source\Deployed; use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Config\DocumentRoot as BaseDocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; /** * Document root detector. @@ -15,25 +14,18 @@ * @api * @since 101.0.0 * - * @deprecated Use new implementation - * @see \Magento\Framework\Config\DocumentRoot + * @deprecated Magento always uses the pub directory + * @see DirectoryList::PUB */ class DocumentRoot { - /** - * @var BaseDocumentRoot - */ - private $documentRoot; - /** * @param DeploymentConfig $config - * @param BaseDocumentRoot $documentRoot * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function __construct(DeploymentConfig $config, BaseDocumentRoot $documentRoot = null) + public function __construct(DeploymentConfig $config) { - $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(BaseDocumentRoot::class); } /** @@ -44,7 +36,7 @@ public function __construct(DeploymentConfig $config, BaseDocumentRoot $document */ public function getPath() { - return $this->documentRoot->getPath(); + return DirectoryList::PUB; } /** @@ -55,6 +47,6 @@ public function getPath() */ public function isPub() { - return $this->documentRoot->isPub(); + return true; } } diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml index 6b4f2351b8b10..871571a049875 100644 --- a/app/code/Magento/MediaGalleryRenditions/etc/config.xml +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -13,6 +13,11 @@ <width>1000</width> <height>1000</height> </media_gallery_renditions> + <media_storage_configuration> + <allowed_resources> + <renditions_folder>.renditions</renditions_folder> + </allowed_resources> + </media_storage_configuration> </system> </default> </config> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 48f2aad8fa746..19c2569695d56 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -82,24 +82,25 @@ public function execute(string $path): AssetInterface * SPL file info is not compatible with remote storages and must not be used. */ $file = $this->getFileInfo->execute($absolutePath); + [$width, $height] = getimagesize($absolutePath); $meta = [ 'size' => $file->getSize(), 'extension' => $file->getExtension(), 'basename' => $file->getBasename(), + 'extra' => [ + 'image-width' => $width, + 'image-height' => $height + ] ]; } - [$width, $height] = getimagesizefromstring( - $this->getMediaDirectory()->readFile($absolutePath) - ); - return $this->assetFactory->create( [ 'id' => null, 'path' => $path, 'title' => $meta['basename'], - 'width' => $width, - 'height' => $height, + 'width' => $meta['extra']['image-width'], + 'height' => $meta['extra']['image-height'], 'hash' => $this->getHash($path), 'size' => $meta['size'], 'contentType' => 'image/' . $meta['extension'], diff --git a/app/code/Magento/MediaStorage/Model/File/Storage.php b/app/code/Magento/MediaStorage/Model/File/Storage.php index f93b9180fa23d..f5ebda4a8d55c 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage.php @@ -82,6 +82,13 @@ class Storage extends AbstractModel */ protected $_databaseFactory; + /** + * @var Filesystem + * + * @deprecated + */ + protected $filesystem; + /** * @var Filesystem\Directory\ReadInterface */ @@ -122,6 +129,8 @@ public function __construct( $this->_fileFlag = $fileFlag; $this->_fileFactory = $fileFactory; $this->_databaseFactory = $databaseFactory; + $this->filesystem = $filesystem; + $this->localMediaDirectory = $filesystem->getDirectoryRead( DirectoryList::MEDIA, Filesystem\DriverPool::FILE diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php deleted file mode 100644 index e87ca584299e7..0000000000000 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Console\Command; - -use Magento\Framework\App\DeploymentConfig\Writer; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Console\Cli; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem\DriverPool; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Remote storage configuration disablement. - */ -class RemoteStorageDisableCommand extends Command -{ - private const NAME = 'remote-storage:disable'; - - /** - * @var Writer - */ - private $writer; - - /** - * @param Writer $writer - */ - public function __construct(Writer $writer) - { - $this->writer = $writer; - - parent::__construct(); - } - - /** - * @inheritDoc - */ - protected function configure(): void - { - $this->setName(self::NAME) - ->setDescription('Disable remote storage'); - } - - /** - * Executes command. - * - * @param InputInterface $input - * @param OutputInterface $output - * @return int - * @throws FileSystemException - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->writer->saveConfig([ - ConfigFilePool::APP_ENV => [ - 'remote_storage' => [ - 'driver' => DriverPool::FILE, - ] - ] - ], true); - - $output->writeln('<info>Config was saved.</info>'); - - return Cli::RETURN_SUCCESS; - } -} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php deleted file mode 100644 index bc21700cadee0..0000000000000 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php +++ /dev/null @@ -1,144 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Console\Command; - -use Magento\Framework\App\DeploymentConfig\Writer; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Console\Cli; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem\DriverPool; -use Magento\RemoteStorage\Driver\DriverFactoryPool; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Remote storage configuration enablement. - */ -class RemoteStorageEnableCommand extends Command -{ - private const NAME = 'remote-storage:enable'; - private const ARG_DRIVER = 'driver'; - private const ARGUMENT_BUCKET = 'bucket'; - private const ARGUMENT_REGION = 'region'; - private const OPTION_ACCESS_KEY = 'access-key'; - private const OPTION_SECRET_KEY = 'secret-key'; - private const ARGUMENT_PREFIX = 'prefix'; - private const OPTION_IS_PUBLIC = 'is-public'; - - /** - * @var Writer - */ - private $writer; - - /** - * @var DriverFactoryPool - */ - private $driverFactoryPool; - - /** - * @param Writer $writer - * @param DriverFactoryPool $driverFactoryPool - */ - public function __construct(Writer $writer, DriverFactoryPool $driverFactoryPool) - { - $this->writer = $writer; - $this->driverFactoryPool = $driverFactoryPool; - - parent::__construct(); - } - - /** - * @inheritDoc - */ - protected function configure(): void - { - $this->setName(self::NAME) - ->setDescription('Enable remote storage integration') - ->addArgument(self::ARG_DRIVER, InputArgument::OPTIONAL, 'Remote driver', DriverPool::FILE) - ->addArgument(self::ARGUMENT_BUCKET, InputArgument::OPTIONAL, 'Bucket') - ->addArgument(self::ARGUMENT_REGION, InputArgument::OPTIONAL, 'Region') - ->addArgument(self::ARGUMENT_PREFIX, InputArgument::OPTIONAL, 'Prefix', '') - ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_OPTIONAL, 'Access key') - ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_OPTIONAL, 'Secret key') - ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_REQUIRED, 'Is public', false); - } - - /** - * Executes command. - * - * @param InputInterface $input - * @param OutputInterface $output - * @return int - * @throws FileSystemException - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $driver = $input->getArgument(self::ARG_DRIVER); - - if ($driver === DriverPool::FILE) { - $output->writeln(sprintf( - 'Driver "%s" was specified. Skipping', - $driver - )); - - return Cli::RETURN_SUCCESS; - } - - if (!$this->driverFactoryPool->has($driver)) { - $output->writeln('Driver %s was not found', $driver); - - return Cli::RETURN_FAILURE; - } - - $prefix = (string)$input->getArgument(self::ARGUMENT_PREFIX); - $config = [ - 'bucket' => (string)$input->getArgument(self::ARGUMENT_BUCKET), - 'region' => (string)$input->getArgument(self::ARGUMENT_REGION), - ]; - $isPublic = (bool)$input->getOption(self::OPTION_IS_PUBLIC); - - if (($key = (string)$input->getOption(self::OPTION_ACCESS_KEY)) - && ($secret = (string)$input->getOption(self::OPTION_SECRET_KEY)) - ) { - $config['credentials']['key'] = $key; - $config['credentials']['secret'] = $secret; - } - - try { - $this->driverFactoryPool->get($driver)->create($config, $prefix); - } catch (\Exception $exception) { - $output->writeln(sprintf( - '<error>Config cannot be set: %s</error>', - $exception->getMessage() - )); - - return Cli::RETURN_FAILURE; - } - - $this->writer->saveConfig([ - ConfigFilePool::APP_ENV => [ - 'remote_storage' => [ - 'driver' => $driver, - 'prefix' => $prefix, - 'is_public' => $isPublic, - 'config' => $config - ] - ] - ], true); - - $output->writeln(sprintf( - '<info>Config for driver "%s" was saved.</info>', - $driver - )); - - return Cli::RETURN_SUCCESS; - } -} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php new file mode 100644 index 0000000000000..a53a203b6d550 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\LocalizedException; +use Magento\RemoteStorage\Model\Synchronizer; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\RemoteStorage\Model\Config; + +/** + * Synchronizes local storage with remote storage. + */ +class RemoteStorageSynchronizeCommand extends Command +{ + private const NAME = 'remote-storage:sync'; + + /** + * @var Synchronizer + */ + private $synchronizer; + + /** + * @var Config + */ + private $config; + + /** + * @param Synchronizer $synchronizer + * @param Config $config + */ + public function __construct( + Synchronizer $synchronizer, + Config $config + ) { + $this->synchronizer = $synchronizer; + $this->config = $config; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setDescription('Synchronize media files with remote storage.'); + } + + /** + * Run synchronization. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws LocalizedException + */ + public function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->config->isEnabled()) { + $output->writeln('<error>Remote storage is not enabled.</error>'); + + return Cli::RETURN_FAILURE; + } + + $output->writeln('<info>Uploading media files to remote storage.</info>'); + + foreach ($this->synchronizer->execute() as $file) { + $output->writeln('- ' . $file); + } + + $output->writeln('<info>End of upload.</info>'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index 731ec6686e657..0c085da78ddac 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -17,7 +17,7 @@ /** * The remote driver pool. */ -class DriverPool implements DriverPoolInterface +class DriverPool extends BaseDriverPool implements DriverPoolInterface { public const PATH_DRIVER = 'remote_storage/driver'; public const PATH_EXPOSE_URLS = 'remote_storage/expose_urls'; @@ -39,11 +39,6 @@ class DriverPool implements DriverPoolInterface */ private $driverFactoryPool; - /** - * @var DriverPool - */ - private $driverPool; - /** * @var array */ @@ -52,25 +47,27 @@ class DriverPool implements DriverPoolInterface /** * @param Config $config * @param DriverFactoryPool $driverFactoryPool - * @param BaseDriverPool $driverPool + * @param array $extraTypes */ public function __construct( Config $config, DriverFactoryPool $driverFactoryPool, - BaseDriverPool $driverPool + array $extraTypes = [] ) { $this->config = $config; $this->driverFactoryPool = $driverFactoryPool; - $this->driverPool = $driverPool; + + parent::__construct($extraTypes); } /** * Retrieves remote driver. * * @param string $code - * @return RemoteDriverInterface - * @throws RuntimeException + * @return DriverInterface + * @throws DriverException * @throws FileSystemException + * @throws RuntimeException */ public function getDriver($code = self::REMOTE): DriverInterface { @@ -91,6 +88,6 @@ public function getDriver($code = self::REMOTE): DriverInterface throw new RuntimeException(__('Remote driver is not available.')); } - return $this->driverPool->getDriver($code); + return parent::getDriver($code); } } diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php index f2d5237ea243b..01af39cfc50a3 100644 --- a/app/code/Magento/RemoteStorage/Filesystem.php +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -16,7 +16,7 @@ /** * Filesystem implementation for remote storage. */ -class Filesystem extends BaseFilesystem +class Filesystem extends BaseFilesystem implements FilesystemInterface { /** * @var bool @@ -120,4 +120,12 @@ public function getDirectoryReadByPath($path, $driverCode = DriverPool::REMOTE) return parent::getDirectoryReadByPath($path); } + + /** + * @inheritDoc + */ + public function getDirectoryCodes(): array + { + return $this->directoryCodes; + } } diff --git a/app/code/Magento/RemoteStorage/FilesystemInterface.php b/app/code/Magento/RemoteStorage/FilesystemInterface.php new file mode 100644 index 0000000000000..42669200c0caf --- /dev/null +++ b/app/code/Magento/RemoteStorage/FilesystemInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage; + +/** + * Provides extension for applicable directory codes. + */ +interface FilesystemInterface +{ + /** + * Retrieve directory codes. + */ + public function getDirectoryCodes(): array; +} diff --git a/app/code/Magento/RemoteStorage/Model/Synchronizer.php b/app/code/Magento/RemoteStorage/Model/Synchronizer.php new file mode 100644 index 0000000000000..4276c7a1a2ffd --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Synchronizer.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model; + +use Generator; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\Glob; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\RemoteStorage\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; + +/** + * Synchronize files from local filesystem. + */ +class Synchronizer +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->filesystem = $filesystem; + } + + /** + * File upload. + * + * @return Generator + * @throws FileSystemException + * @throws ValidatorException + */ + public function execute(): Generator + { + foreach ($this->filesystem->getDirectoryCodes() as $directoryCode) { + $directory = $this->filesystem->getDirectoryWrite($directoryCode, DriverPool::FILE); + $remoteDirectory = $this->filesystem->getDirectoryWrite($directoryCode, RemoteDriverPool::REMOTE); + + yield from $this->copyRecursive($directory, $remoteDirectory, $directory->getAbsolutePath()); + } + } + + /** + * Recursive file upload. + * + * @param WriteInterface $directory + * @param WriteInterface $remoteDirectory + * @param string $path + * @param string $pattern + * @param int $flags + * @return Generator + * @throws FileSystemException + */ + private function copyRecursive( + WriteInterface $directory, + WriteInterface $remoteDirectory, + string $path, + string $pattern = '*.*', + int $flags = Glob::GLOB_NOSORT + ): Generator { + $path = rtrim($path, '/'); + $localDriver = $directory->getDriver(); + $remoteDriver = $remoteDirectory->getDriver(); + + foreach (Glob::glob($path . '/' . $pattern, $flags) as $file) { + /** + * Extracting relative path in local system to apply it for remote system. + */ + $relativeFile = $directory->getRelativePath($file); + $destination = $remoteDirectory->getAbsolutePath($relativeFile); + + if (!$remoteDirectory->isExist($destination)) { + $localDriver->copy($file, $destination, $remoteDriver); + + yield $relativeFile; + } + } + + foreach (Glob::glob($path . '/{,.}[!.,!..]*', + $flags | Glob::GLOB_ONLYDIR | Glob::GLOB_BRACE) as $childDirectory) { + $relativeDirectory = $directory->getRelativePath($childDirectory); + $destinationDirectory = $remoteDirectory->getAbsolutePath($relativeDirectory); + + if (!$remoteDirectory->isDirectory($destinationDirectory)) { + $remoteDriver->createDirectory($destinationDirectory); + + yield $relativeDirectory; + } + + yield from $this->copyRecursive($directory, $remoteDirectory, $childDirectory, $pattern, $flags); + } + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php new file mode 100644 index 0000000000000..5c3ddb74bb0cf --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Test\Unit\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\RemoteStorage\Filesystem; +use Magento\RemoteStorage\Model\Synchronizer; +use PHPUnit\Framework\TestCase; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; + +/** + * @see Synchronizer + */ +class SynchronizerTest extends TestCase +{ + /** + * @var Synchronizer + */ + private $synchronizer; + + /** + * @var Filesystem + */ + private $filesystemMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->filesystemMock = $this->createMock(Filesystem::class); + + $this->synchronizer = new Synchronizer( + $this->filesystemMock + ); + } + + /** + * @throws FileSystemException + * @throws ValidatorException + */ + public function testExecute(): void + { + $this->filesystemMock->method('getDirectoryCodes') + ->willReturn(['test']); + + $localDriver = $this->createMock(DriverInterface::class); + $remoteDriver = $this->createMock(DriverInterface::class); + + $localDirectory = $this->createMock(WriteInterface::class); + $localDirectory->method('getDriver') + ->willReturn($localDriver); + $remoteDirectory = $this->createMock(WriteInterface::class); + $remoteDirectory->method('getDriver') + ->willReturn($remoteDriver); + + $this->filesystemMock->method('getDirectoryWrite') + ->willReturnMap([ + ['test', DriverPool::FILE, $localDirectory], + ['test', RemoteDriverPool::REMOTE, $remoteDirectory] + ]); + $localDirectory->method('getAbsolutePath') + ->willReturnMap([ + [null, __DIR__ . '/_files/test'] + ]); + $localDirectory->method('getRelativePath') + ->willReturnCallback(function ($arg) { + return str_replace(__DIR__, '', $arg); + }); + $remoteDirectory->expects(self::exactly(2)) + ->method('isExist') + ->willReturnMap([ + [ + 'remote:/_files/test/root_file.txt', + false + ], + [ + 'remote:/_files/test/.dot_directory/child_file.txt', + true + ] + ]); + $remoteDirectory->method('getAbsolutePath') + ->willReturnCallback(function ($arg) { + return 'remote:' . $arg; + }); + $localDriver->expects(self::once()) + ->method('copy') + ->withConsecutive( + [__DIR__ . '/_files/test/root_file.txt', 'remote:/_files/test/root_file.txt', $remoteDriver] + ); + + self::assertSame( + [ + '/_files/test/root_file.txt', + '/_files/test/.dot_directory' + ], + iterator_to_array($this->synchronizer->execute(), false) + ); + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 9684a3ac49dbc..9fdde517b952c 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -27,7 +27,6 @@ <argument name="directoryCodes" xsi:type="array"> <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> <item name="var_export" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::VAR_EXPORT</item> - <item name="var_import" xsi:type="string">Magento\Framework\App\Filesystem\DirectoryList::VAR_IMPORT</item> </argument> </arguments> </virtualType> @@ -62,8 +61,7 @@ <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> - <item name="remoteStorageEnable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageEnableCommand</item> - <item name="remoteStorageDisable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageDisableCommand</item> + <item name="remoteStorageSync" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageSynchronizeCommand</item> </argument> </arguments> </type> @@ -131,4 +129,9 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> + <type name="Magento\RemoteStorage\Model\Synchronizer"> + <arguments> + <argument name="filesystem" xsi:type="object">customRemoteFilesystem</argument> + </arguments> + </type> </config> diff --git a/app/etc/di.xml b/app/etc/di.xml index d1282ff3ab961..fe7d86c4f599d 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -212,7 +212,6 @@ <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> <preference for="Magento\Framework\Interception\ConfigLoaderInterface" type="Magento\Framework\Interception\PluginListGenerator" /> <preference for="Magento\Framework\Interception\ConfigWriterInterface" type="Magento\Framework\Interception\PluginListGenerator" /> - <preference for="Magento\Framework\Filesystem\DriverPoolInterface" type="Magento\Framework\Filesystem\DriverPool" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php deleted file mode 100644 index d20604e27e5c9..0000000000000 --- a/lib/internal/Magento/Framework/Config/DocumentRoot.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Framework\Config; - -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\App\DeploymentConfig; - -/** - * Document root detector. - * @deprecated Magento always uses the pub directory - * @api - */ -class DocumentRoot -{ - /** - * @var DeploymentConfig - */ - private $config; - - /** - * @param DeploymentConfig $config - */ - public function __construct(DeploymentConfig $config) - { - $this->config = $config; - } - - /** - * A shortcut to load the document root path from the DirectoryList. - * - * @return string - */ - public function getPath(): string - { - return DirectoryList::PUB; - } - - /** - * Checks if root folder is /pub. - * - * @return bool - */ - public function isPub(): bool - { - return true; - } -} diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 706d6efef44b9..c3246bfbf1e48 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -7,7 +7,6 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\TargetDirectory; @@ -188,11 +187,6 @@ class Uploader */ private $targetDirectory; - /** - * @var DocumentRoot - */ - private $documentRoot; - /** * Init upload * @@ -201,7 +195,6 @@ class Uploader * @param DirectoryList|null $directoryList * @param DriverPool|null $driverPool * @param TargetDirectory|null $targetDirectory - * @param DocumentRoot|null $documentRoot * @throws \DomainException */ public function __construct( @@ -209,8 +202,7 @@ public function __construct( Mime $fileMime = null, DirectoryList $directoryList = null, DriverPool $driverPool = null, - TargetDirectory $targetDirectory = null, - DocumentRoot $documentRoot = null + TargetDirectory $targetDirectory = null ) { $this->directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); @@ -224,7 +216,6 @@ public function __construct( $this->fileMime = $fileMime ?: ObjectManager::getInstance()->get(Mime::class); $this->driverPool = $driverPool ?: ObjectManager::getInstance()->get(DriverPool::class); $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); - $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); } /** @@ -342,7 +333,7 @@ protected function chmod($file) */ protected function _moveFile($tmpPath, $destPath) { - $rootCode = $this->getDocumentRoot()->getPath(); + $rootCode = DirectoryList::PUB; if (strpos($destPath, $this->getDirectoryList()->getPath($rootCode)) !== 0) { $rootCode = DirectoryList::ROOT; @@ -372,20 +363,6 @@ private function getTargetDirectory(): TargetDirectory return $this->targetDirectory; } - /** - * Retrieves document root. - * - * @return DocumentRoot - */ - private function getDocumentRoot(): DocumentRoot - { - if (!isset($this->documentRoot)) { - $this->documentRoot = ObjectManager::getInstance()->get(DocumentRoot::class); - } - - return $this->documentRoot; - } - /** * Retrieves directory list. * diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php index a3364d3be1c8c..33b025389945a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php @@ -6,7 +6,6 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\Filesystem\DriverPoolInterface; /** * The factory of the filesystem directory instances for read operations. @@ -16,16 +15,16 @@ class ReadFactory /** * Pool of filesystem drivers * - * @var DriverPoolInterface + * @var DriverPool */ private $driverPool; /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php index 6f6bfe558176d..1cb642c5c3a2a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php @@ -6,7 +6,6 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\Filesystem\DriverPoolInterface; /** * The factory of the filesystem directory instances for write operations. @@ -16,16 +15,16 @@ class WriteFactory /** * Pool of filesystem drivers * - * @var DriverPoolInterface + * @var DriverPool */ private $driverPool; /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 07a0d1345a301..bc08f67228849 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -5,13 +5,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\Filesystem\Driver; use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem\Driver\File\Mime; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Filesystem\ExtendedDriverInterface; use Magento\Framework\Filesystem\Glob; use Magento\Framework\Phrase; @@ -22,7 +19,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class File implements ExtendedDriverInterface +class File implements DriverInterface { /** * @var string @@ -83,26 +80,6 @@ public function stat($path) return $result; } - /** - * @inheritDoc - */ - public function getMetadata(string $path): array - { - $fileInfo = new \SplFileInfo($path); - $mime = new Mime(); - - return [ - 'path' => $fileInfo->getPath(), - 'basename' => $fileInfo->getBasename('.' . $fileInfo->getExtension()), - 'extension' => $fileInfo->getExtension(), - 'filename' => $fileInfo->getFilename(), - 'dirname' => dirname($fileInfo->getFilename()), - 'timestamp' => $fileInfo->getMTime(), - 'size' => $fileInfo->getSize(), - 'mimetype' => $mime->getMimeType($path) - ]; - } - /** * Check permissions for reading file or directory * @@ -354,7 +331,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) public function copy($source, $destination, DriverInterface $targetDriver = null) { $targetDriver = $targetDriver ?: $this; - if (get_class($targetDriver) == get_class($this)) { + if (get_class($targetDriver) === get_class($this)) { $result = @copy($this->getScheme() . $source, $destination); } else { $content = $this->fileGetContents($source); diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPool.php b/lib/internal/Magento/Framework/Filesystem/DriverPool.php index dfd1e2abce01a..435e51c26b012 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverPool.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverPool.php @@ -9,7 +9,7 @@ /** * A pool of stream wrappers */ -class DriverPool implements DriverPoolInterface +class DriverPool { /**#@+ * Available driver types diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php index b88db7518ba17..d99b285be0f67 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php @@ -8,7 +8,7 @@ namespace Magento\Framework\Filesystem; /** - * A pool of stream wrappers + * A pool of stream wrappers. */ interface DriverPoolInterface { @@ -18,5 +18,5 @@ interface DriverPoolInterface * @param string $code * @return DriverInterface */ - public function getDriver($code); + public function getDriver($code): DriverInterface; } diff --git a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php index a93d242dbe15a..c2643d7c54e79 100644 --- a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php @@ -7,6 +7,8 @@ namespace Magento\Framework\Filesystem; +use Magento\Framework\Exception\FileSystemException; + /** * Provides extension for Driver interface. * @@ -22,7 +24,7 @@ interface ExtendedDriverInterface extends DriverInterface * * Implementation must return associative array with next keys: * - * ```php + * ``` * [ * 'path', * 'dirname', @@ -32,10 +34,15 @@ interface ExtendedDriverInterface extends DriverInterface * 'timestamp', * 'size', * 'mimetype', - * ]; + * 'extra' => [ + * 'image-width', + * 'image-height' + * ] + * ]; * * @param string $path Absolute path to file * @return array + * @throws FileSystemException * * @deprecated Method will be moved to DriverInterface */ diff --git a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php index b442d6d1c05c3..e46b00bc5c74f 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Filesystem\File; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Filesystem\DriverPoolInterface; +use Magento\Framework\Filesystem\DriverPool; /** * Opens a file for reading @@ -18,16 +18,16 @@ class ReadFactory /** * Pool of filesystem drivers * - * @var DriverPoolInterface + * @var DriverPool */ private $driverPool; /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index 8bc62cb6573c4..7a9596586f56a 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -7,7 +7,6 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\Filesystem\DriverPoolInterface; /** * Opens a file for reading and/or writing @@ -26,9 +25,9 @@ class WriteFactory extends ReadFactory /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { parent::__construct($driverPool); $this->driverPool = $driverPool; From 3afc8136243efb7915a1a7f122298845d4520413 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Tue, 10 Nov 2020 10:34:51 -0600 Subject: [PATCH 195/195] MC-37933: Product Export File Does Not Show In Admin - deleted redundant test --- .../Export/Product/RowCustomizerTest.php | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php deleted file mode 100644 index 110451aa19f1a..0000000000000 --- a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\DownloadableImportExport\Test\Unit\Model\Export\Product; - -use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\Downloadable\Model\LinkRepository; -use Magento\Downloadable\Model\Product\Type as Type; -use Magento\Downloadable\Model\SampleRepository; -use Magento\DownloadableImportExport\Model\Export\RowCustomizer; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Class to test Customizes output during export - */ -class RowCustomizerTest extends TestCase -{ - /** - * @var LinkRepository|MockObject - */ - private $linkRepository; - - /** - * @var SampleRepository|MockObject - */ - private $sampleRepository; - - /** - * @var StoreManagerInterface|MockObject - */ - private $storeManager; - - /** - * @var RowCustomizer - */ - private $rowCustomizer; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->linkRepository = $this->createMock(LinkRepository::class); - $this->sampleRepository = $this->createMock(SampleRepository::class); - $this->storeManager = $this->createMock(StoreManagerInterface::class); - - $this->rowCustomizer = new RowCustomizer( - $this->storeManager, - $this->linkRepository, - $this->sampleRepository - ); - } - - /** - * Test to Prepare downloadable data for export - */ - public function testPrepareData() - { - $productIds = [1, 2, 3]; - $collection = $this->createMock(ProductCollection::class); - $collection->expects($this->at(0)) - ->method('addAttributeToFilter') - ->with('entity_id', ['in' => $productIds]) - ->willReturnSelf(); - $collection->expects($this->at(1)) - ->method('addAttributeToFilter') - ->with('type_id', ['eq' => Type::TYPE_DOWNLOADABLE]) - ->willReturnSelf(); - $collection->method('addAttributeToSelect')->willReturnSelf(); - $collection->method('getIterator')->willReturn(new \ArrayIterator([])); - - $this->storeManager->expects($this->once()) - ->method('setCurrentStore') - ->with(Store::DEFAULT_STORE_ID); - - $this->rowCustomizer->prepareData($collection, $productIds); - } -}