From 5a7e865d2d693cee84427d9906c85c0a2ec05ac7 Mon Sep 17 00:00:00 2001 From: Maksym Aposov Date: Tue, 26 May 2020 17:06:03 -0500 Subject: [PATCH 01/65] MC-34427: Cleanup travis configuration --- .github/CONTRIBUTING.md | 2 +- .htaccess | 9 - .htaccess.sample | 9 - .../etc/install-config-mysql.travis.php.dist | 23 --- .../testFromCreateProject/composer.lock | 8 - .../_files/testSkeleton/composer.lock | 8 - .../testsuite/Magento/MemoryUsageTest.php | 2 +- .../Magento/Phpserver/PhpserverTest.php | 58 +++--- .../Model/_files/testSkeleton/composer.lock | 4 - dev/travis/before_install.sh | 63 ------- dev/travis/before_script.sh | 168 ------------------ dev/travis/config/apache_virtual_host | 20 --- dev/travis/config/www.conf | 14 -- 13 files changed, 32 insertions(+), 356 deletions(-) delete mode 100644 dev/tests/integration/etc/install-config-mysql.travis.php.dist delete mode 100755 dev/travis/before_install.sh delete mode 100755 dev/travis/before_script.sh delete mode 100644 dev/travis/config/apache_virtual_host delete mode 100644 dev/travis/config/www.conf diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 37b7bc2ca8c3a..397a106a4574b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,7 +23,7 @@ For more detailed information on contribution please read our [beginners guide]( * Unit/integration test coverage * Proposed [documentation](https://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). 4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. -5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). +5. All automated tests must pass. ## Contribution process diff --git a/.htaccess b/.htaccess index e07a564bc0ab6..c5f3bf034d2fb 100644 --- a/.htaccess +++ b/.htaccess @@ -238,15 +238,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny diff --git a/.htaccess.sample b/.htaccess.sample index c9e83a53cc8bd..776f9046cf11d 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -238,15 +238,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny diff --git a/dev/tests/integration/etc/install-config-mysql.travis.php.dist b/dev/tests/integration/etc/install-config-mysql.travis.php.dist deleted file mode 100644 index 8c41b0a0f2626..0000000000000 --- a/dev/tests/integration/etc/install-config-mysql.travis.php.dist +++ /dev/null @@ -1,23 +0,0 @@ - '127.0.0.1', - 'db-user' => 'root', - 'db-password' => '', - 'db-name' => 'magento_integration_tests', - 'db-prefix' => 'trv_', - 'backend-frontname' => 'backend', - 'admin-user' => \Magento\TestFramework\Bootstrap::ADMIN_NAME, - 'admin-password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, - 'admin-email' => \Magento\TestFramework\Bootstrap::ADMIN_EMAIL, - 'admin-firstname' => \Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME, - 'admin-lastname' => \Magento\TestFramework\Bootstrap::ADMIN_LASTNAME, - 'amqp-host' => 'localhost', - 'amqp-port' => '5672', - 'amqp-user' => 'guest', - 'amqp-password' => 'guest', -]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock index fbcaf8d1600fb..2e363a5ce3a8b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock @@ -2400,10 +2400,6 @@ ".php_cs.dist", ".php_cs.dist" ], - [ - ".travis.yml", - ".travis.yml" - ], [ ".user.ini", ".user.ini" @@ -2652,10 +2648,6 @@ "dev/tools", "dev/tools" ], - [ - "dev/travis", - "dev/travis" - ], [ "generated", "generated" diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock index 6c51c6a2072e9..75eb51fd62112 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock @@ -2400,10 +2400,6 @@ ".php_cs.dist", ".php_cs.dist" ], - [ - ".travis.yml", - ".travis.yml" - ], [ ".user.ini", ".user.ini" @@ -2652,10 +2648,6 @@ "dev/tools", "dev/tools" ], - [ - "dev/travis", - "dev/travis" - ], [ "generated", "generated" diff --git a/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php b/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php index 1cefa80d8f611..cff3bae8bc2e1 100644 --- a/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php +++ b/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php @@ -32,7 +32,7 @@ protected function setUp(): void */ public function testAppReinitializationNoMemoryLeak() { - $this->markTestSkipped('Test fails at Travis. Skipped until MAGETWO-47111'); + $this->markTestSkipped('Skipped until MAGETWO-47111'); $this->_deallocateUnusedMemory(); $actualMemoryUsage = $this->_helper->getRealMemoryUsage(); diff --git a/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php b/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php index e7a1de90fc933..4ae8c51c45a97 100644 --- a/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Phpserver; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + /** * @magentoAppIsolation enabled * @@ -19,33 +22,15 @@ class PhpserverTest extends \PHPUnit\Framework\TestCase { const BASE_URL = '127.0.0.1:8082'; - private static $serverPid; - /** - * @var \Laminas\Http\Client + * @var Process */ - private $httpClient; + private $serverProcess; /** - * Instantiate phpserver in the pub folder + * @var \Laminas\Http\Client */ - public static function setUpBeforeClass(): void - { - if (!(defined('TRAVIS') && TRAVIS === true)) { - self::markTestSkipped('Travis environment test'); - } - $return = []; - - $baseDir = __DIR__ . '/../../../../../../'; - $command = sprintf( - 'cd %s && php -S %s -t ./pub/ ./phpserver/router.php >/dev/null 2>&1 & echo $!', - $baseDir, - static::BASE_URL - ); - // phpcs:ignore - exec($command, $return); - static::$serverPid = (int) $return[0]; - } + private $httpClient; private function getUrl($url) { @@ -55,11 +40,33 @@ private function getUrl($url) protected function setUp(): void { $this->httpClient = new \Laminas\Http\Client(null, ['timeout' => 10]); + + /** @var Process $process */ + $phpBinaryFinder = new PhpExecutableFinder(); + $phpBinaryPath = $phpBinaryFinder->find(); + $command = sprintf( + "%s -S %s -t ./pub ./phpserver/router.php", + $phpBinaryPath, + self::BASE_URL + ); + $this->serverProcess = Process::fromShellCommandline( + $command, + realpath(__DIR__ . '/../../../../../../') + ); + $this->serverProcess->start(); + $this->serverProcess->waitUntil(function ($type, $output) { + return strpos($output, "Development Server") !== false; + }); + } + + protected function tearDown(): void + { + $this->serverProcess->stop(); } public function testServerHasPid() { - $this->assertTrue(static::$serverPid > 0); + $this->assertTrue($this->serverProcess->getPid() > 0); } public function testServerResponds() @@ -86,9 +93,4 @@ public function testStaticImageFile() $this->assertFalse($response->isClientError()); $this->assertStringStartsWith('image/gif', $response->getHeaders()->get('Content-Type')->getMediaType()); } - - public static function tearDownAfterClass(): void - { - posix_kill(static::$serverPid, SIGKILL); - } } diff --git a/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock b/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock index 4027a0f2c41c3..b22e038559e85 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock @@ -871,10 +871,6 @@ "LICENSE.txt", "LICENSE.txt" ], - [ - ".travis.yml", - ".travis.yml" - ], [ "app/bootstrap.php", "app/bootstrap.php" diff --git a/dev/travis/before_install.sh b/dev/travis/before_install.sh deleted file mode 100755 index 845d70e4e79fd..0000000000000 --- a/dev/travis/before_install.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash - -# Copyright © Magento, Inc. All rights reserved. -# See COPYING.txt for license details. - -set -e -trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit code $?' ERR - -# mock mail -sudo service postfix stop -echo # print a newline -smtp-sink -d "%d.%H.%M.%S" localhost:2500 1000 & -echo 'sendmail_path = "/usr/sbin/sendmail -t -i "' > ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/sendmail.ini - -# disable xdebug and adjust memory limit -echo > ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini -echo 'memory_limit = -1' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini -phpenv rehash; - -# If env var is present, configure support for 3rd party builds which include private dependencies -test -n "$GITHUB_TOKEN" && composer config github-oauth.github.com "$GITHUB_TOKEN" || true - -# Node.js setup via NVM -if [ $TEST_SUITE == "js" ]; then - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm - - nvm install $NODE_JS_VERSION - nvm use $NODE_JS_VERSION - node --version - - npm install -g yarn - yarn global add grunt-cli -fi - -if [ $TEST_SUITE = "functional" ] || [ $TEST_SUITE = "graphql-api-functional" ]; then - # Install apache - sudo apt-get update - sudo apt-get install apache2 libapache2-mod-fastcgi - if [ ${TRAVIS_PHP_VERSION:0:1} == "7" ]; then - sudo cp ${TRAVIS_BUILD_DIR}/dev/travis/config/www.conf ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.d/ - fi - - # Enable php-fpm - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf - sudo a2enmod rewrite actions fastcgi alias - echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm - - # Configure apache virtual hosts - sudo cp -f ${TRAVIS_BUILD_DIR}/dev/travis/config/apache_virtual_host /etc/apache2/sites-available/000-default.conf - sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/000-default.conf - sudo sed -e "s?%MAGENTO_HOST_NAME%?${MAGENTO_HOST_NAME}?g" --in-place /etc/apache2/sites-available/000-default.conf - - sudo usermod -a -G www-data travis - sudo usermod -a -G travis www-data - - phpenv config-rm xdebug.ini - sudo service apache2 restart - - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -screen 0 1280x1024x24 -fi diff --git a/dev/travis/before_script.sh b/dev/travis/before_script.sh deleted file mode 100755 index 5d091efbb30a3..0000000000000 --- a/dev/travis/before_script.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash - -# Copyright © Magento, Inc. All rights reserved. -# See COPYING.txt for license details. - -set -e -trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit code $?' ERR - -# prepare for test suite -case $TEST_SUITE in - integration) - cd dev/tests/integration - - test_set_list=$(find testsuite/* -maxdepth 1 -mindepth 1 -type d | sort) - test_set_count=$(printf "$test_set_list" | wc -l) - test_set_size[1]=$(printf "%.0f" $(echo "$test_set_count*0.13" | bc)) #13% - test_set_size[2]=$(printf "%.0f" $(echo "$test_set_count*0.30" | bc)) #30% - test_set_size[3]=$((test_set_count-test_set_size[1]-test_set_size[2])) #55% - echo "Total = ${test_set_count}; Batch #1 = ${test_set_size[1]}; Batch #2 = ${test_set_size[2]}; Batch #3 = ${test_set_size[3]};"; - - echo "==> preparing integration testsuite on index $INTEGRATION_INDEX with set size of ${test_set_size[$INTEGRATION_INDEX]}" - cp phpunit.xml.dist phpunit.xml - - # remove memory usage tests if from any set other than the first - if [[ $INTEGRATION_INDEX > 1 ]]; then - echo " - removing testsuite/Magento/MemoryUsageTest.php" - perl -pi -0e 's#^\s+ + From 86ec81c9567dced0bc6a284b3dca76809c748f33 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Fri, 3 Jul 2020 16:28:51 +0300 Subject: [PATCH 08/65] magento/magento2-login-as-customer#188: Introduce Login as Customer "is enabled" check extensibility. --- ...LoginAsCustomerEnabledForCustomerChain.php | 66 ++++++++++++++++ ...oginAsCustomerEnabledForCustomerResult.php | 53 +++++++++++++ .../IsLoginAsCustomerEnabledResolver.php | 54 +++++++++++++ app/code/Magento/LoginAsCustomer/etc/di.xml | 32 ++++++-- .../Controller/Adminhtml/Login/Login.php | 32 ++++++-- .../Plugin/Button/ToolbarPlugin.php | 67 +++++++++------- .../Component/Button/DataProvider.php | 79 +++++++++++++++++++ .../Control/LoginAsCustomerButton.php | 42 +++------- .../etc/adminhtml/di.xml | 12 ++- .../Magento/LoginAsCustomerAdminUi/etc/di.xml | 31 ++++++++ ...tomerEnabledForCustomerResultInterface.php | 37 +++++++++ ...nAsCustomerEnabledForCustomerInterface.php | 26 ++++++ 12 files changed, 459 insertions(+), 72 deletions(-) create mode 100644 app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerChain.php create mode 100644 app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php create mode 100644 app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php create mode 100644 app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php create mode 100644 app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml create mode 100644 app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php create mode 100644 app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php diff --git a/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerChain.php b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerChain.php new file mode 100644 index 0000000000000..6937a11eb3b58 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerChain.php @@ -0,0 +1,66 @@ +config = $config; + $this->resultFactory = $resultFactory; + $this->resolvers = $resolvers; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = [[]]; + /** @var IsLoginAsCustomerEnabledForCustomerResultInterface $resolver */ + foreach ($this->resolvers as $resolver) { + $resolverResult = $resolver->execute($customerId); + if (!$resolverResult->isEnabled()) { + $messages[] = $resolverResult->getMessages(); + } + } + + return $this->resultFactory->create(['messages' => array_merge(...$messages)]); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php new file mode 100644 index 0000000000000..0d2af8669777c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php @@ -0,0 +1,53 @@ +messages = $messages; + } + + /** + * @inheritdoc + */ + public function isEnabled(): bool + { + return empty($this->messages); + } + + /** + * @inheritdoc + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * @inheritdoc + */ + public function setMessages(array $messages): void + { + $this->messages = $messages; + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php b/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php new file mode 100644 index 0000000000000..de16a798983c0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php @@ -0,0 +1,54 @@ +config = $config; + $this->resultFactory = $resultFactory; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = []; + if (!$this->config->isEnabled()) { + $messages[] = __('Login as Customer is disabled.'); + } + + return $this->resultFactory->create(['messages' => $messages]); + } +} diff --git a/app/code/Magento/LoginAsCustomer/etc/di.xml b/app/code/Magento/LoginAsCustomer/etc/di.xml index c0ba4901ba7b8..d4728bc0c8f42 100755 --- a/app/code/Magento/LoginAsCustomer/etc/di.xml +++ b/app/code/Magento/LoginAsCustomer/etc/di.xml @@ -5,14 +5,32 @@ * See COPYING.txt for license details. */ --> - - - - - - + + + + + + - + + + + + + + Magento\LoginAsCustomer\Model\Resolver\IsLoginAsCustomerEnabledResolver + + + + diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 70eef5347f8e3..27189cb2682e9 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -12,6 +12,7 @@ use Magento\Backend\Model\Auth\Session; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; @@ -22,6 +23,7 @@ use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterfaceFactory; use Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; use Magento\Store\Model\StoreManagerInterface; @@ -80,6 +82,11 @@ class Login extends Action implements HttpGetActionInterface */ private $url; + /** + * @var IsLoginAsCustomerEnabledForCustomerInterface + */ + private $isLoginAsCustomerEnabled; + /** * @param Context $context * @param Session $authSession @@ -87,9 +94,11 @@ class Login extends Action implements HttpGetActionInterface * @param CustomerRepositoryInterface $customerRepository * @param ConfigInterface $config * @param AuthenticationDataInterfaceFactory $authenticationDataFactory - * @param SaveAuthenticationDataInterface $saveAuthenticationData , + * @param SaveAuthenticationDataInterface $saveAuthenticationData * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser * @param Url $url + * @param IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -100,7 +109,8 @@ public function __construct( AuthenticationDataInterfaceFactory $authenticationDataFactory, SaveAuthenticationDataInterface $saveAuthenticationData, DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, - Url $url + Url $url, + ?IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled = null ) { parent::__construct($context); @@ -112,6 +122,8 @@ public function __construct( $this->saveAuthenticationData = $saveAuthenticationData; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; + $this->isLoginAsCustomerEnabled = $isLoginAsCustomerEnabled + ?? ObjectManager::getInstance()->get(IsLoginAsCustomerEnabledForCustomerInterface::class); } /** @@ -126,20 +138,24 @@ public function execute(): ResultInterface /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->config->isEnabled()) { - $this->messageManager->addErrorMessage(__('Login as Customer is disabled.')); - return $resultRedirect->setPath('customer/index/index'); - } - $customerId = (int)$this->_request->getParam('customer_id'); if (!$customerId) { $customerId = (int)$this->_request->getParam('entity_id'); } + $isLoginAsCustomerEnabled = $this->isLoginAsCustomerEnabled->execute($customerId); + if (!$isLoginAsCustomerEnabled->isEnabled()) { + foreach ($isLoginAsCustomerEnabled->getMessages() as $message) { + $this->messageManager->addErrorMessage(__($message)); + } + + return $resultRedirect->setPath('customer/index/index'); + } + try { $customer = $this->customerRepository->getById($customerId); } catch (NoSuchEntityException $e) { - $this->messageManager->addErrorMessage(__('Customer with this ID are no longer exist.')); + $this->messageManager->addErrorMessage('Customer with this ID are no longer exist.'); return $resultRedirect->setPath('customer/index/index'); } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php index 89ee2791e38af..3516a181b5dde 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php @@ -9,9 +9,10 @@ use Magento\Backend\Block\Widget\Button\ButtonList; use Magento\Backend\Block\Widget\Button\Toolbar; -use Magento\Framework\View\Element\AbstractBlock; -use Magento\Framework\Escaper; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Escaper; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; use Magento\LoginAsCustomerApi\Api\ConfigInterface; /** @@ -34,20 +35,28 @@ class ToolbarPlugin */ private $config; + /** + * @var DataProvider + */ + private $dataProvider; + /** * ToolbarPlugin constructor. * @param AuthorizationInterface $authorization * @param ConfigInterface $config * @param Escaper $escaper + * @param DataProvider $dataProvider */ public function __construct( AuthorizationInterface $authorization, ConfigInterface $config, - Escaper $escaper + Escaper $escaper, + DataProvider $dataProvider ) { $this->authorization = $authorization; $this->config = $config; $this->escaper = $escaper; + $this->dataProvider = $dataProvider; } /** @@ -62,10 +71,35 @@ public function beforePushButtons( Toolbar $subject, AbstractBlock $context, ButtonList $buttonList - ):void { - $order = false; + ): void { $nameInLayout = $context->getNameInLayout(); + $order = $this->getOrder($nameInLayout, $context); + if ($order + && !empty($order['customer_id']) + && $this->config->isEnabled() + && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button') + ) { + $customerId = (int)$order['customer_id']; + $buttonList->add( + 'guest_to_customer', + $this->dataProvider->getData($customerId), + -1 + ); + } + } + + /** + * Extract order data from context. + * + * @param string $nameInLayout + * @param AbstractBlock $context + * @return array|null + */ + private function getOrder(string $nameInLayout, AbstractBlock $context) + { + $order = null; + if ('sales_order_edit' == $nameInLayout) { $order = $context->getOrder(); } elseif ('sales_invoice_view' == $nameInLayout) { @@ -75,28 +109,7 @@ public function beforePushButtons( } elseif ('sales_creditmemo_view' == $nameInLayout) { $order = $context->getCreditmemo()->getOrder(); } - if ($order) { - $isAllowed = $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); - $isEnabled = $this->config->isEnabled(); - if ($isAllowed && $isEnabled) { - if (!empty($order['customer_id'])) { - $buttonUrl = $context->getUrl('loginascustomer/login/login', [ - 'customer_id' => $order['customer_id'] - ]); - $buttonList->add( - 'guest_to_customer', - [ - 'label' => __('Login as Customer'), - 'onclick' => 'window.lacConfirmationPopup("' - . $this->escaper->escapeHtml($this->escaper->escapeJs($buttonUrl)) - . '")', - 'class' => 'reset' - ], - -1 - ); - } - } - } + return $order; } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php new file mode 100644 index 0000000000000..19db8c67f945e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php @@ -0,0 +1,79 @@ +escaper = $escaper; + $this->urlBuilder = $urlBuilder; + $this->data = $data; + } + + /** + * Get data for Login as Customer button. + * + * @param int $customerId + * @return array + */ + public function getData(int $customerId): array + { + $buttonData = [ + 'on_click' => 'window.lacConfirmationPopup("' + . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl($customerId))) + . '")', + ]; + + return array_merge_recursive($buttonData, $this->data); + } + + /** + * Get Login as Customer login url. + * + * @param int $customerId + * @return string + */ + private function getLoginUrl(int $customerId): string + { + return $this->urlBuilder->getUrl('loginascustomer/login/login', ['customer_id' => $customerId]); + } +} diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php index d900641c131a3..ab43fca3d447e 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php @@ -7,12 +7,13 @@ namespace Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control; -use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; -use Magento\Framework\AuthorizationInterface; -use Magento\Framework\Escaper; -use Magento\Framework\Registry; use Magento\Backend\Block\Widget\Context; use Magento\Customer\Block\Adminhtml\Edit\GenericButton; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; use Magento\LoginAsCustomerApi\Api\ConfigInterface; /** @@ -31,26 +32,26 @@ class LoginAsCustomerButton extends GenericButton implements ButtonProviderInter private $config; /** - * Escaper - * - * @var Escaper + * @var DataProvider */ - private $escaper; + private $dataProvider; /** * @param Context $context * @param Registry $registry * @param ConfigInterface $config + * @param DataProvider $dataProvider */ public function __construct( Context $context, Registry $registry, - ConfigInterface $config + ConfigInterface $config, + ?DataProvider $dataProvider = null ) { parent::__construct($context, $registry); $this->authorization = $context->getAuthorization(); $this->config = $config; - $this->escaper = $context->getEscaper(); + $this->dataProvider = $dataProvider ?? ObjectManager::getInstance()->get(DataProvider::class); } /** @@ -58,31 +59,14 @@ public function __construct( */ public function getButtonData(): array { - $customerId = $this->getCustomerId(); + $customerId = (int)$this->getCustomerId(); $data = []; $isAllowed = $customerId && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); $isEnabled = $this->config->isEnabled(); if ($isAllowed && $isEnabled) { - $data = [ - 'label' => __('Login as Customer'), - 'class' => 'login login-button', - 'on_click' => 'window.lacConfirmationPopup("' - . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl())) - . '")', - 'sort_order' => 15, - ]; + $data = $this->dataProvider->getData($customerId); } return $data; } - - /** - * Get Login as Customer login url. - * - * @return string - */ - public function getLoginUrl(): string - { - return $this->getUrl('loginascustomer/login/login', ['customer_id' => $this->getCustomerId()]); - } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml index dabab45205527..b73a1d856c888 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml @@ -6,7 +6,17 @@ */ --> - + + + + Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton\DataProvider + + + + + Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin\DataProvider + + diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml new file mode 100644 index 0000000000000..8ba8c5c6ead43 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml @@ -0,0 +1,31 @@ + + + + + + + + Login as Customer + login login-button + 15 + + + + + + + + Login as Customer + reset + + + + diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php new file mode 100644 index 0000000000000..4f517fd7f315f --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php @@ -0,0 +1,37 @@ + Date: Mon, 6 Jul 2020 13:16:45 +0300 Subject: [PATCH 09/65] magento/magento2-login-as-customer#190: No order comments are created when admin is logged in as customer and place order with Multiple Addresses. --- .../Magento/LoginAsCustomerSales/etc/frontend/di.xml | 12 ++++++++++++ .../LoginAsCustomerSales/etc/webapi_rest/di.xml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml diff --git a/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml new file mode 100644 index 0000000000000..1a010fcdead85 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml index 1a010fcdead85..6dda349f1e60d 100644 --- a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml +++ b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml @@ -7,6 +7,6 @@ --> - + From f1f9ce7cf919b676b115c526403aab1091988e74 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Tue, 7 Jul 2020 15:13:47 -0500 Subject: [PATCH 10/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- .../Framework/DB/Adapter/AdapterInterface.php | 10 +++- .../Framework/DB/Adapter/Pdo/Mysql.php | 13 +++++ .../Setup/Console/Command/UpgradeCommand.php | 58 ++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index f654fd263f605..3a7fc59a005e7 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -36,7 +36,7 @@ interface AdapterInterface const INSERT_ON_DUPLICATE = 1; const INSERT_IGNORE = 2; - + /** Strategy for updating data in table. See https://dev.mysql.com/doc/refman/5.7/en/replace.html */ const REPLACE = 4; @@ -1127,6 +1127,14 @@ public function dropTrigger($triggerName, $schemaName = null); */ public function getTables($likeCondition = null); + /** + * Retrieve triggers list + * + * @param null|string $likeCondition + * @return array + */ + public function getTriggers($likeCondition = null); + /** * Generates case SQL fragment * diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 7db91c06d9649..51c639e4f7476 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -4038,6 +4038,19 @@ public function getTables($likeCondition = null) return $tables; } + /** + * Retrieve triggers list + * + * @param null|string $likeCondition + * @return array + */ + public function getTriggers($likeCondition = null) + { + $sql = ($likeCondition === null) ? 'SHOW TRIGGERS' : sprintf("SHOW TRIGGERS LIKE '%s'", $likeCondition); + $result = $this->query($sql); + return $result->fetchAll(); + } + /** * Returns auto increment field if exists * diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index 4a0cd3bc9a69a..03b0d03538c9b 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -6,9 +6,13 @@ namespace Magento\Setup\Console\Command; use Magento\Deploy\Console\Command\App\ConfigImportCommand; -use Magento\Framework\App\State as AppState; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\State as AppState; +use Magento\Framework\DB\Ddl\Trigger; +use Magento\Framework\Mview\View\CollectionInterface as ViewCollection; +use Magento\Framework\Mview\View\StateInterface; use Magento\Framework\Setup\ConsoleLogger; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\Declaration\Schema\OperationsExecutor; @@ -52,22 +56,30 @@ class UpgradeCommand extends AbstractSetupCommand */ private $searchConfigFactory; + /* + * @var ViewCollection + */ + private $viewCollection; + /** * @param InstallerFactory $installerFactory * @param SearchConfigFactory $searchConfigFactory * @param DeploymentConfig $deploymentConfig * @param AppState|null $appState + * @param ViewCollection|null $viewCollection */ public function __construct( InstallerFactory $installerFactory, SearchConfigFactory $searchConfigFactory, DeploymentConfig $deploymentConfig = null, - AppState $appState = null + AppState $appState = null, + ViewCollection $viewCollection = null ) { $this->installerFactory = $installerFactory; $this->searchConfigFactory = $searchConfigFactory; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->appState = $appState ?: ObjectManager::getInstance()->get(AppState::class); + $this->viewCollection = $viewCollection ?: ObjectManager::getInstance()->get(ViewCollection::class); parent::__construct(); } @@ -130,6 +142,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $installer->updateModulesSequence($keepGenerated); $searchConfig = $this->searchConfigFactory->create(); $searchConfig->validateSearchEngine(); + + $this->removeUnusedTriggers(); + $installer->installSchema($request); $installer->installDataFixtures($request); @@ -157,4 +172,43 @@ protected function execute(InputInterface $input, OutputInterface $output) return \Magento\Framework\Console\Cli::RETURN_SUCCESS; } + + /** + * Remove unused triggers + */ + private function removeUnusedTriggers() + { + // unsubscribe mview + $viewList = $this->viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + foreach ($viewList as $view) { + /** @var \Magento\Framework\Mview\ViewInterface $view */ + $view->unsubscribe(); + } + + // remove extra triggers that have correct naming structure + /* @var ResourceConnection $resource */ + $resource = ObjectManager::getInstance()->get(ResourceConnection::class); + $connection = $resource->getConnection(); + $triggers = $connection->getTriggers(); + foreach ($triggers as $trigger) { + $triggerNames = []; + foreach (Trigger::getListOfEvents() as $event) { + $triggerName = $resource->getTriggerName( + $resource->getTableName($trigger['Table']), + Trigger::TIME_AFTER, + $event + ); + $triggerNames[] = strtolower($triggerName); + } + if (in_array($trigger['Trigger'], $triggerNames)) { + $connection->dropTrigger($trigger['Trigger']); + } + } + + // subscribe mview + foreach ($viewList as $view) { + /** @var \Magento\Framework\Mview\ViewInterface $view */ + $view->subscribe(); + } + } } From 289d609911cd607f1cf698747f5ecc9c0dec134c Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Wed, 8 Jul 2020 15:13:06 +0300 Subject: [PATCH 11/65] magento/magento2-login-as-customer#186: [CE] Admin cannot login as customer in two different customers account --- .../Plugin/InvalidateExpiredSessionPlugin.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php index b68e871c5f955..37aad2cef8998 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php @@ -61,7 +61,9 @@ public function beforeExecute(ActionInterface $subject) $customerId = (int)$this->session->getCustomerId(); if ($adminId && $customerId) { if (!$this->isLoginAsCustomerSessionActive->execute($customerId, $adminId)) { - $this->session->destroy(); + $this->session->clearStorage(); + $this->session->expireSessionCookie(); + $this->session->regenerateId(); } } } From 4b0f7ede4caca5d3a63ad0b6c7f0ad6f353894ea Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Thu, 9 Jul 2020 13:26:02 -0500 Subject: [PATCH 12/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- .../Setup/Console/Command/UpgradeCommand.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index 03b0d03538c9b..ad183cfc72f39 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -11,7 +11,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\App\State as AppState; use Magento\Framework\DB\Ddl\Trigger; -use Magento\Framework\Mview\View\CollectionInterface as ViewCollection; +use Magento\Framework\Mview\View\CollectionFactory as ViewCollectionFactory; use Magento\Framework\Mview\View\StateInterface; use Magento\Framework\Setup\ConsoleLogger; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; @@ -57,29 +57,29 @@ class UpgradeCommand extends AbstractSetupCommand private $searchConfigFactory; /* - * @var ViewCollection + * @var ViewCollectionFactory */ - private $viewCollection; + private $viewCollectionFactory; /** * @param InstallerFactory $installerFactory * @param SearchConfigFactory $searchConfigFactory * @param DeploymentConfig $deploymentConfig * @param AppState|null $appState - * @param ViewCollection|null $viewCollection + * @param ViewCollectionFactory|null $viewCollectionFactory */ public function __construct( InstallerFactory $installerFactory, SearchConfigFactory $searchConfigFactory, DeploymentConfig $deploymentConfig = null, AppState $appState = null, - ViewCollection $viewCollection = null + ViewCollectionFactory $viewCollectionFactory = null ) { $this->installerFactory = $installerFactory; $this->searchConfigFactory = $searchConfigFactory; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->appState = $appState ?: ObjectManager::getInstance()->get(AppState::class); - $this->viewCollection = $viewCollection ?: ObjectManager::getInstance()->get(ViewCollection::class); + $this->viewCollectionFactory = $viewCollectionFactory ?: ObjectManager::getInstance()->get(ViewCollectionFactory::class); parent::__construct(); } @@ -179,14 +179,15 @@ protected function execute(InputInterface $input, OutputInterface $output) private function removeUnusedTriggers() { // unsubscribe mview - $viewList = $this->viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + $viewCollection = $this->viewCollectionFactory->create(); + $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); foreach ($viewList as $view) { /** @var \Magento\Framework\Mview\ViewInterface $view */ $view->unsubscribe(); } // remove extra triggers that have correct naming structure - /* @var ResourceConnection $resource */ + /** @var ResourceConnection $resource */ $resource = ObjectManager::getInstance()->get(ResourceConnection::class); $connection = $resource->getConnection(); $triggers = $connection->getTriggers(); From b26a48ce4322dcc4503803fbf6418298ac20c530 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Fri, 10 Jul 2020 13:07:52 -0500 Subject: [PATCH 13/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- .../Framework/DB/Adapter/AdapterInterface.php | 8 -- .../Framework/DB/Adapter/Pdo/Mysql.php | 13 --- .../Setup/Console/Command/UpgradeCommand.php | 103 ++++++++++++------ 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index 3a7fc59a005e7..f5064c892a7b2 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -1127,14 +1127,6 @@ public function dropTrigger($triggerName, $schemaName = null); */ public function getTables($likeCondition = null); - /** - * Retrieve triggers list - * - * @param null|string $likeCondition - * @return array - */ - public function getTriggers($likeCondition = null); - /** * Generates case SQL fragment * diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 51c639e4f7476..7db91c06d9649 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -4038,19 +4038,6 @@ public function getTables($likeCondition = null) return $tables; } - /** - * Retrieve triggers list - * - * @param null|string $likeCondition - * @return array - */ - public function getTriggers($likeCondition = null) - { - $sql = ($likeCondition === null) ? 'SHOW TRIGGERS' : sprintf("SHOW TRIGGERS LIKE '%s'", $likeCondition); - $result = $this->query($sql); - return $result->fetchAll(); - } - /** * Returns auto increment field if exists * diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index ad183cfc72f39..8e6879454db27 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -10,9 +10,13 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\App\State as AppState; -use Magento\Framework\DB\Ddl\Trigger; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Mview\View\CollectionFactory as ViewCollectionFactory; use Magento\Framework\Mview\View\StateInterface; +use Magento\Framework\Mview\View\SubscriptionFactory; +use Magento\Framework\Mview\View\SubscriptionInterface; +use Magento\Framework\Mview\ViewInterface; use Magento\Framework\Setup\ConsoleLogger; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\Declaration\Schema\OperationsExecutor; @@ -56,30 +60,48 @@ class UpgradeCommand extends AbstractSetupCommand */ private $searchConfigFactory; - /* + /** * @var ViewCollectionFactory */ private $viewCollectionFactory; + /** + * @var SubscriptionFactory + */ + private $subscriptionFactory; + + /** + * @var ResourceConnection + */ + private $resource; + /** * @param InstallerFactory $installerFactory * @param SearchConfigFactory $searchConfigFactory * @param DeploymentConfig $deploymentConfig * @param AppState|null $appState * @param ViewCollectionFactory|null $viewCollectionFactory + * @param SubscriptionFactory|null $subscriptionFactory + * @param ResourceConnection|null $resource */ public function __construct( InstallerFactory $installerFactory, SearchConfigFactory $searchConfigFactory, DeploymentConfig $deploymentConfig = null, AppState $appState = null, - ViewCollectionFactory $viewCollectionFactory = null + ViewCollectionFactory $viewCollectionFactory = null, + SubscriptionFactory $subscriptionFactory = null, + ResourceConnection $resource = null ) { $this->installerFactory = $installerFactory; $this->searchConfigFactory = $searchConfigFactory; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->appState = $appState ?: ObjectManager::getInstance()->get(AppState::class); - $this->viewCollectionFactory = $viewCollectionFactory ?: ObjectManager::getInstance()->get(ViewCollectionFactory::class); + $this->viewCollectionFactory = $viewCollectionFactory + ?: ObjectManager::getInstance()->get(ViewCollectionFactory::class); + $this->subscriptionFactory = $subscriptionFactory + ?: ObjectManager::getInstance()->get(SubscriptionFactory::class); + $this->resource = $resource ?: ObjectManager::getInstance()->get(ResourceConnection::class); parent::__construct(); } @@ -142,9 +164,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $installer->updateModulesSequence($keepGenerated); $searchConfig = $this->searchConfigFactory->create(); $searchConfig->validateSearchEngine(); - $this->removeUnusedTriggers(); - $installer->installSchema($request); $installer->installDataFixtures($request); @@ -153,8 +173,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $arrayInput = new ArrayInput([]); $arrayInput->setInteractive($input->isInteractive()); $result = $importConfigCommand->run($arrayInput, $output); - if ($result === \Magento\Framework\Console\Cli::RETURN_FAILURE) { - throw new \Magento\Framework\Exception\RuntimeException( + if ($result === Cli::RETURN_FAILURE) { + throw new RuntimeException( __('%1 failed. See previous output.', ConfigImportCommand::COMMAND_NAME) ); } @@ -167,10 +187,10 @@ protected function execute(InputInterface $input, OutputInterface $output) } } catch (\Exception $e) { $output->writeln('' . $e->getMessage() . ''); - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + return Cli::RETURN_FAILURE; } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + return Cli::RETURN_SUCCESS; } /** @@ -178,38 +198,59 @@ protected function execute(InputInterface $input, OutputInterface $output) */ private function removeUnusedTriggers() { - // unsubscribe mview $viewCollection = $this->viewCollectionFactory->create(); $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + + // Unsubscribe mviews foreach ($viewList as $view) { - /** @var \Magento\Framework\Mview\ViewInterface $view */ + /** @var ViewInterface $view */ $view->unsubscribe(); } - // remove extra triggers that have correct naming structure - /** @var ResourceConnection $resource */ - $resource = ObjectManager::getInstance()->get(ResourceConnection::class); - $connection = $resource->getConnection(); - $triggers = $connection->getTriggers(); + // Remove extra triggers that have correct naming structure + $triggers = $this->getTriggers(); foreach ($triggers as $trigger) { - $triggerNames = []; - foreach (Trigger::getListOfEvents() as $event) { - $triggerName = $resource->getTriggerName( - $resource->getTableName($trigger['Table']), - Trigger::TIME_AFTER, - $event - ); - $triggerNames[] = strtolower($triggerName); - } - if (in_array($trigger['Trigger'], $triggerNames)) { - $connection->dropTrigger($trigger['Trigger']); - } + $this->initSubscriptionInstance($trigger['Table'])->remove(); } - // subscribe mview + // Subscribe mviews foreach ($viewList as $view) { - /** @var \Magento\Framework\Mview\ViewInterface $view */ + /** @var ViewInterface $view */ $view->subscribe(); } } + + /** + * Retrieve triggers list + * + * @return array + */ + private function getTriggers(): array + { + $connection = $this->resource->getConnection(); + $result = $connection->query('SHOW TRIGGERS'); + return $result->fetchAll(); + } + + /** + * Initializes subscription instance + * + * @param string $tablename + * @return SubscriptionInterface + */ + private function initSubscriptionInstance(string $tablename): SubscriptionInterface + { + /** @var ViewInterface $view */ + $view = ObjectManager::getInstance()->create(ViewInterface::class); + $view->setId('0'); + + return $this->subscriptionFactory->create( + [ + 'view' => $view, + 'tableName' => $tablename, + 'columnName' => '', + 'subscriptionModel' => SubscriptionFactory::INSTANCE_NAME, + ] + ); + } } From 45bd9f6aa23b19289e5689d5fcbbe86a7d12d9e9 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Fri, 10 Jul 2020 15:32:57 -0500 Subject: [PATCH 14/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index f5064c892a7b2..f654fd263f605 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -36,7 +36,7 @@ interface AdapterInterface const INSERT_ON_DUPLICATE = 1; const INSERT_IGNORE = 2; - + /** Strategy for updating data in table. See https://dev.mysql.com/doc/refman/5.7/en/replace.html */ const REPLACE = 4; From cd1397d6badbdcb2aa56bdc9429fa431cab0d2fc Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Tue, 14 Jul 2020 08:09:59 -0500 Subject: [PATCH 15/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- .../Magento/Framework/Mview/OldViews.php | 116 ++++++++++++++++++ .../Setup/Console/Command/UpgradeCommand.php | 97 +-------------- setup/src/Magento/Setup/Model/Installer.php | 15 +++ 3 files changed, 133 insertions(+), 95 deletions(-) create mode 100644 lib/internal/Magento/Framework/Mview/OldViews.php diff --git a/lib/internal/Magento/Framework/Mview/OldViews.php b/lib/internal/Magento/Framework/Mview/OldViews.php new file mode 100644 index 0000000000000..1ae5c65df52e3 --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/OldViews.php @@ -0,0 +1,116 @@ +viewCollectionFactory = $viewCollectionFactory; + $this->resource = $resource; + $this->viewFactory = $viewFactory; + } + + /** + * Remove unused triggers + */ + public function unsubscribe() + { + $viewCollection = $this->viewCollectionFactory->create(); + $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + + // Unsubscribe mviews + foreach ($viewList as $view) { + /** @var ViewInterface $view */ + $view->unsubscribe(); + } + + // Unsubscribe old views that still have triggers in db + $triggerTableNames = $this->getTriggerTableNames(); + foreach ($triggerTableNames as $tableName) { + $this->createViewByTableName($tableName)->unsubscribe(); + } + + // Re-subscribe mviews + foreach ($viewList as $view) { + /** @var ViewInterface $view */ + $view->subscribe(); + } + } + + /** + * Retrieve trigger table name list + * + * @return array + */ + private function getTriggerTableNames(): array + { + $connection = $this->resource->getConnection(); + $dbName = $this->resource->getSchemaName(ResourceConnection::DEFAULT_CONNECTION); + $sql = $connection->select() + ->from( + ['information_schema.TRIGGERS'], + ['EVENT_OBJECT_TABLE'] + ) + ->distinct(true) + ->where('TRIGGER_SCHEMA = ?', $dbName); + return $connection->fetchCol($sql); + } + + /** + * Create view by db table name + * + * @param string $tableName + * @return ViewInterface + */ + private function createViewByTableName(string $tableName): ViewInterface + { + $subscription[$tableName] = [ + 'name' => $tableName, + 'column' => '', + 'subscription_model' => null + ]; + $data['data'] = [ + 'id' => '0', + 'subscriptions' => $subscription, + ]; + + $view = $this->viewFactory->create($data); + $view->getState()->setMode(StateInterface::MODE_ENABLED); + + return $view; + } +} diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index 8e6879454db27..af25466fa141d 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -8,15 +8,9 @@ use Magento\Deploy\Console\Command\App\ConfigImportCommand; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\ObjectManager; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\App\State as AppState; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\RuntimeException; -use Magento\Framework\Mview\View\CollectionFactory as ViewCollectionFactory; -use Magento\Framework\Mview\View\StateInterface; -use Magento\Framework\Mview\View\SubscriptionFactory; -use Magento\Framework\Mview\View\SubscriptionInterface; -use Magento\Framework\Mview\ViewInterface; use Magento\Framework\Setup\ConsoleLogger; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\Declaration\Schema\OperationsExecutor; @@ -60,48 +54,22 @@ class UpgradeCommand extends AbstractSetupCommand */ private $searchConfigFactory; - /** - * @var ViewCollectionFactory - */ - private $viewCollectionFactory; - - /** - * @var SubscriptionFactory - */ - private $subscriptionFactory; - - /** - * @var ResourceConnection - */ - private $resource; - /** * @param InstallerFactory $installerFactory * @param SearchConfigFactory $searchConfigFactory * @param DeploymentConfig $deploymentConfig * @param AppState|null $appState - * @param ViewCollectionFactory|null $viewCollectionFactory - * @param SubscriptionFactory|null $subscriptionFactory - * @param ResourceConnection|null $resource */ public function __construct( InstallerFactory $installerFactory, SearchConfigFactory $searchConfigFactory, DeploymentConfig $deploymentConfig = null, - AppState $appState = null, - ViewCollectionFactory $viewCollectionFactory = null, - SubscriptionFactory $subscriptionFactory = null, - ResourceConnection $resource = null + AppState $appState = null ) { $this->installerFactory = $installerFactory; $this->searchConfigFactory = $searchConfigFactory; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->appState = $appState ?: ObjectManager::getInstance()->get(AppState::class); - $this->viewCollectionFactory = $viewCollectionFactory - ?: ObjectManager::getInstance()->get(ViewCollectionFactory::class); - $this->subscriptionFactory = $subscriptionFactory - ?: ObjectManager::getInstance()->get(SubscriptionFactory::class); - $this->resource = $resource ?: ObjectManager::getInstance()->get(ResourceConnection::class); parent::__construct(); } @@ -164,7 +132,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $installer->updateModulesSequence($keepGenerated); $searchConfig = $this->searchConfigFactory->create(); $searchConfig->validateSearchEngine(); - $this->removeUnusedTriggers(); + $installer->removeUnusedTriggers(); $installer->installSchema($request); $installer->installDataFixtures($request); @@ -192,65 +160,4 @@ protected function execute(InputInterface $input, OutputInterface $output) return Cli::RETURN_SUCCESS; } - - /** - * Remove unused triggers - */ - private function removeUnusedTriggers() - { - $viewCollection = $this->viewCollectionFactory->create(); - $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); - - // Unsubscribe mviews - foreach ($viewList as $view) { - /** @var ViewInterface $view */ - $view->unsubscribe(); - } - - // Remove extra triggers that have correct naming structure - $triggers = $this->getTriggers(); - foreach ($triggers as $trigger) { - $this->initSubscriptionInstance($trigger['Table'])->remove(); - } - - // Subscribe mviews - foreach ($viewList as $view) { - /** @var ViewInterface $view */ - $view->subscribe(); - } - } - - /** - * Retrieve triggers list - * - * @return array - */ - private function getTriggers(): array - { - $connection = $this->resource->getConnection(); - $result = $connection->query('SHOW TRIGGERS'); - return $result->fetchAll(); - } - - /** - * Initializes subscription instance - * - * @param string $tablename - * @return SubscriptionInterface - */ - private function initSubscriptionInstance(string $tablename): SubscriptionInterface - { - /** @var ViewInterface $view */ - $view = ObjectManager::getInstance()->create(ViewInterface::class); - $view->setId('0'); - - return $this->subscriptionFactory->create( - [ - 'view' => $view, - 'tableName' => $tablename, - 'columnName' => '', - 'subscriptionModel' => SubscriptionFactory::INSTANCE_NAME, - ] - ); - } } diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index fe1cac3f076fd..13a9eeaafd9ef 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -25,6 +25,7 @@ use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Module\ModuleList\Loader as ModuleLoader; use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\Mview\OldViews; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\FilePermissions; use Magento\Framework\Setup\InstallDataInterface; @@ -247,6 +248,11 @@ class Installer */ private $patchApplierFactory; + /** + * @var OldViews + */ + private $oldViews; + /** * Constructor * @@ -320,6 +326,7 @@ public function __construct( $this->componentRegistrar = $componentRegistrar; $this->phpReadinessCheck = $phpReadinessCheck; $this->schemaPersistor = $this->objectManagerProvider->get()->get(SchemaPersistor::class); + $this->oldViews = $this->objectManagerProvider->get()->get(OldViews::class); } /** @@ -1646,4 +1653,12 @@ private function updateColumnType( ); } } + + /** + * Remove unused triggers from db + */ + public function removeUnusedTriggers(): void + { + $this->oldViews->unsubscribe(); + } } From 48748ce3a945a45a5a3a99762338becc166768d2 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Tue, 14 Jul 2020 08:23:57 -0500 Subject: [PATCH 16/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- lib/internal/Magento/Framework/Mview/OldViews.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Mview/OldViews.php b/lib/internal/Magento/Framework/Mview/OldViews.php index 1ae5c65df52e3..8b78eda2fd774 100644 --- a/lib/internal/Magento/Framework/Mview/OldViews.php +++ b/lib/internal/Magento/Framework/Mview/OldViews.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Mview; use Magento\Framework\App\ResourceConnection; @@ -45,9 +47,9 @@ public function __construct( } /** - * Remove unused triggers + * Unsubscribe old views by existing triggers */ - public function unsubscribe() + public function unsubscribe(): void { $viewCollection = $this->viewCollectionFactory->create(); $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); From ee09909ab5282ca7a8d6d05e133e77af27e67691 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Wed, 15 Jul 2020 13:01:01 -0500 Subject: [PATCH 17/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- .../Magento/Framework/Mview/OldViews.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/internal/Magento/Framework/Mview/OldViews.php b/lib/internal/Magento/Framework/Mview/OldViews.php index 8b78eda2fd774..816ca9bad27cd 100644 --- a/lib/internal/Magento/Framework/Mview/OldViews.php +++ b/lib/internal/Magento/Framework/Mview/OldViews.php @@ -61,9 +61,11 @@ public function unsubscribe(): void } // Unsubscribe old views that still have triggers in db - $triggerTableNames = $this->getTriggerTableNames(); + $triggerTableNames = $this->getTableNamesWithTriggers(); foreach ($triggerTableNames as $tableName) { - $this->createViewByTableName($tableName)->unsubscribe(); + $view = $this->createViewByTableName($tableName); + $view->unsubscribe(); + $view->getState()->delete(); } // Re-subscribe mviews @@ -74,11 +76,11 @@ public function unsubscribe(): void } /** - * Retrieve trigger table name list - * - * @return array - */ - private function getTriggerTableNames(): array + * Retrieve list of table names that have triggers + * + * @return array + */ + private function getTableNamesWithTriggers(): array { $connection = $this->resource->getConnection(); $dbName = $this->resource->getSchemaName(ResourceConnection::DEFAULT_CONNECTION); @@ -106,11 +108,11 @@ private function createViewByTableName(string $tableName): ViewInterface 'subscription_model' => null ]; $data['data'] = [ - 'id' => '0', 'subscriptions' => $subscription, ]; $view = $this->viewFactory->create($data); + $view->setId('old_view'); $view->getState()->setMode(StateInterface::MODE_ENABLED); return $view; From 4148fc2c7442a3804b930074556462ed46c41fe2 Mon Sep 17 00:00:00 2001 From: Max Lesechko Date: Thu, 16 Jul 2020 16:20:01 -0500 Subject: [PATCH 18/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- app/etc/di.xml | 3 ++ .../Magento/Framework/Indexer/Config.php | 29 +++++++++++++++++ .../{OldViews.php => TriggerCleaner.php} | 11 ++++--- .../Mview/TriggerCleanerInterface.php | 20 ++++++++++++ .../Framework/Mview/View/State/Collection.php | 21 +++++++++++++ setup/config/di.config.php | 31 +++++++++++++------ setup/src/Magento/Setup/Model/Installer.php | 10 +++--- 7 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 lib/internal/Magento/Framework/Indexer/Config.php rename lib/internal/Magento/Framework/Mview/{OldViews.php => TriggerCleaner.php} (94%) create mode 100644 lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php create mode 100644 lib/internal/Magento/Framework/Mview/View/State/Collection.php diff --git a/app/etc/di.xml b/app/etc/di.xml index ba635d0662755..93f16b3cd69de 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -209,6 +209,9 @@ + + + diff --git a/lib/internal/Magento/Framework/Indexer/Config.php b/lib/internal/Magento/Framework/Indexer/Config.php new file mode 100644 index 0000000000000..97584379b7ab5 --- /dev/null +++ b/lib/internal/Magento/Framework/Indexer/Config.php @@ -0,0 +1,29 @@ +viewCollectionFactory->create(); $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); // Unsubscribe mviews foreach ($viewList as $view) { - /** @var ViewInterface $view */ $view->unsubscribe(); } @@ -70,9 +70,10 @@ public function unsubscribe(): void // Re-subscribe mviews foreach ($viewList as $view) { - /** @var ViewInterface $view */ $view->subscribe(); } + + return true; } /** diff --git a/lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php b/lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php new file mode 100644 index 0000000000000..5991a5f01c398 --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php @@ -0,0 +1,20 @@ + [ 'instance' => [ 'preference' => [ - \Laminas\EventManager\EventManagerInterface::class => 'EventManager', - \Laminas\ServiceManager\ServiceLocatorInterface::class => \Laminas\ServiceManager\ServiceManager::class, - \Magento\Framework\DB\LoggerInterface::class => \Magento\Framework\DB\Logger\Quiet::class, - \Magento\Framework\Locale\ConfigInterface::class => \Magento\Framework\Locale\Config::class, - \Magento\Framework\Filesystem\DriverInterface::class => - \Magento\Framework\Filesystem\Driver\File::class, - \Magento\Framework\Component\ComponentRegistrarInterface::class => - \Magento\Framework\Component\ComponentRegistrar::class, + EventManagerInterface::class => 'EventManager', + ServiceLocatorInterface::class => ServiceManager::class, + LoggerInterface::class => Quiet::class, + ConfigInterface::class => Config::class, + DriverInterface::class => \Magento\Framework\Filesystem\Driver\File::class, + ComponentRegistrarInterface::class => ComponentRegistrar::class, + TriggerCleanerInterface::class => TriggerCleaner::class, ], - \Magento\Framework\Setup\Declaration\Schema\SchemaConfig::class => [ + SchemaConfig::class => [ 'parameters' => [ 'connectionScopes' => [ 'default', diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index 13a9eeaafd9ef..955d3b2dc1365 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -25,7 +25,7 @@ use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Module\ModuleList\Loader as ModuleLoader; use Magento\Framework\Module\ModuleListInterface; -use Magento\Framework\Mview\OldViews; +use Magento\Framework\Mview\TriggerCleanerInterface; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\FilePermissions; use Magento\Framework\Setup\InstallDataInterface; @@ -249,9 +249,9 @@ class Installer private $patchApplierFactory; /** - * @var OldViews + * @var TriggerCleanerInterface */ - private $oldViews; + private $triggerCleaner; /** * Constructor @@ -326,7 +326,7 @@ public function __construct( $this->componentRegistrar = $componentRegistrar; $this->phpReadinessCheck = $phpReadinessCheck; $this->schemaPersistor = $this->objectManagerProvider->get()->get(SchemaPersistor::class); - $this->oldViews = $this->objectManagerProvider->get()->get(OldViews::class); + $this->triggerCleaner = $this->objectManagerProvider->get()->get(TriggerCleanerInterface::class); } /** @@ -1659,6 +1659,6 @@ private function updateColumnType( */ public function removeUnusedTriggers(): void { - $this->oldViews->unsubscribe(); + $this->triggerCleaner->unsubscribe(); } } From cea922aab3e156e5cc55195f4de181a4f650bd04 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Tue, 21 Jul 2020 07:50:26 -0500 Subject: [PATCH 19/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- lib/internal/Magento/Framework/Mview/View/Collection.php | 6 +++++- setup/src/Magento/Setup/Model/Installer.php | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Mview/View/Collection.php b/lib/internal/Magento/Framework/Mview/View/Collection.php index 91276bb376049..573bbf798d49e 100644 --- a/lib/internal/Magento/Framework/Mview/View/Collection.php +++ b/lib/internal/Magento/Framework/Mview/View/Collection.php @@ -93,7 +93,11 @@ private function getOrderedViewIds() /** @var IndexerInterface $indexer */ foreach (array_keys($this->indexerConfig->getIndexers()) as $indexerId) { $indexer = $this->_entityFactory->create(IndexerInterface::class); - $orderedViewIds[] = $indexer->load($indexerId)->getViewId(); + $viewId = $indexer->load($indexerId)->getViewId(); + $view = $this->config->getView($viewId); + if (!empty($view) && !empty($view['view_id']) && $view['view_id'] === $viewId) { + $orderedViewIds[] = $viewId; + } } $orderedViewIds = array_filter($orderedViewIds); $orderedViewIds += array_diff(array_keys($this->config->getViews()), $orderedViewIds); diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index 955d3b2dc1365..ad94f2e62b819 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -1660,5 +1660,6 @@ private function updateColumnType( public function removeUnusedTriggers(): void { $this->triggerCleaner->unsubscribe(); + $this->cleanCaches(); } } From d9099231abd515f1fa8ba9a64b7cfa575db23f8e Mon Sep 17 00:00:00 2001 From: Max Lesechko Date: Tue, 21 Jul 2020 14:30:08 -0500 Subject: [PATCH 20/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- app/etc/di.xml | 3 - dev/tests/integration/bin/magento | 8 +- .../TestFramework/Console/CliProxy.php | 123 ++++++++++++++++++ .../etc/di/preferences/cli/ce.php | 9 ++ .../TestFramework/Deploy/CliCommand.php | 62 ++++++--- .../Mview/DummyTriggerCleaner.php | 24 ++++ .../setup-integration/framework/bootstrap.php | 10 +- .../Magento/Framework/Indexer/Config.php | 29 ----- .../Framework/Mview/TriggerCleaner.php | 8 +- .../Mview/TriggerCleanerInterface.php | 20 --- .../Framework/Mview/View/State/Collection.php | 21 --- setup/config/di.config.php | 3 - setup/src/Magento/Setup/Model/Installer.php | 89 +++++++++---- 13 files changed, 285 insertions(+), 124 deletions(-) create mode 100644 dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php create mode 100644 dev/tests/setup-integration/etc/di/preferences/cli/ce.php create mode 100644 dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php delete mode 100644 lib/internal/Magento/Framework/Indexer/Config.php delete mode 100644 lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php delete mode 100644 lib/internal/Magento/Framework/Mview/View/State/Collection.php diff --git a/app/etc/di.xml b/app/etc/di.xml index 93f16b3cd69de..ba635d0662755 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -209,9 +209,6 @@ - - - diff --git a/dev/tests/integration/bin/magento b/dev/tests/integration/bin/magento index 303fbfb217d2b..4a86eaee01e83 100755 --- a/dev/tests/integration/bin/magento +++ b/dev/tests/integration/bin/magento @@ -5,6 +5,9 @@ * See COPYING.txt for license details. */ +use Magento\Framework\Console\Cli; +use Magento\TestFramework\Console\CliProxy; + if (PHP_SAPI !== 'cli') { echo 'bin/magento must be run as a CLI application'; exit(1); @@ -21,7 +24,8 @@ if (isset($_SERVER['INTEGRATION_TEST_PARAMS'])) { } try { - require $_SERVER['MAGE_DIRS']['base']['path'] . '/app/bootstrap.php'; + require $_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER'] ?? + ($_SERVER['MAGE_DIRS']['base']['path'] . '/app/bootstrap.php'); } catch (\Exception $e) { echo 'Autoload error: ' . $e->getMessage(); exit(1); @@ -29,7 +33,7 @@ try { try { $handler = new \Magento\Framework\App\ErrorHandler(); set_error_handler([$handler, 'handler']); - $application = new Magento\Framework\Console\Cli('Magento CLI'); + $application = new CliProxy('Magento CLI'); $application->run(); } catch (\Exception $e) { while ($e) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php new file mode 100644 index 0000000000000..d47931f4734ab --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php @@ -0,0 +1,123 @@ +subject = new Cli($name, $version); + $this->injectDiConfiguration($this->subject); + } + + /** + * Runs the current application. + * + * @see \Magento\Framework\Console\Cli::doRun + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null + * @throws \Exception + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + return $this->getSubject()->doRun($input, $output); + } + + /** + * Runs the current application. + * + * @see \Symfony\Component\Console\Application::run + * @param InputInterface|null $input + * @param OutputInterface|null $output + * @return int + * @throws \Exception + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + return $this->getSubject()->run($input, $output); + } + + /** + * Get subject + * + * @return Cli + */ + private function getSubject(): Cli + { + return $this->subject; + } + + /** + * Inject additional DI configuration + * + * @param Cli $cli + * @return bool + * @throws LocalizedException + * @throws \ReflectionException + */ + private function injectDiConfiguration(Cli $cli): bool + { + $diPreferences = $this->getDiPreferences(); + if ($diPreferences) { + $object = new \ReflectionObject($cli); + + $attribute = $object->getProperty('objectManager'); + $attribute->setAccessible(true); + + /** @var ObjectManagerInterface $objectManager */ + $objectManager = $attribute->getValue($cli); + $objectManager->configure($diPreferences); + + $attribute->setAccessible(false); + } + + return true; + } + + /** + * Get additional DI preferences + * + * @return array|array[] + * @throws LocalizedException + * @SuppressWarnings(PHPMD.Superglobals) + */ + private function getDiPreferences(): array + { + $diPreferences = []; + $diPreferencesPath = $_SERVER['TESTS_BASE_DIR'] . '/etc/di/preferences/cli/'; + + $preferenceFiles = glob($diPreferencesPath . '*.php'); + + foreach ($preferenceFiles as $file) { + if (!is_readable($file)) { + throw new LocalizedException(__("'%1' is not readable file.", $file)); + } + $diPreferences = array_replace($diPreferences, include $file); + } + + return $diPreferences ? ['preferences' => $diPreferences] : $diPreferences; + } +} diff --git a/dev/tests/setup-integration/etc/di/preferences/cli/ce.php b/dev/tests/setup-integration/etc/di/preferences/cli/ce.php new file mode 100644 index 0000000000000..b5605d98206f3 --- /dev/null +++ b/dev/tests/setup-integration/etc/di/preferences/cli/ce.php @@ -0,0 +1,9 @@ + '\Magento\TestFramework\Mview\DummyTriggerCleaner', +]; 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 10a839df83a5e..9507e50d71638 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php @@ -6,17 +6,20 @@ namespace Magento\TestFramework\Deploy; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Shell; use Magento\Framework\Shell\CommandRenderer; use Magento\Setup\Console\Command\InstallCommand; /** * The purpose of this class is enable/disable module and upgrade commands execution. + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class CliCommand { /** - * @var \Magento\Framework\Shell + * @var Shell */ private $shell; @@ -36,10 +39,7 @@ class CliCommand private $deploymentConfig; /** - * ShellCommand constructor. - * - * @param TestModuleManager $testEnv - * @param DeploymentConfig $deploymentConfig + * @param TestModuleManager $testEnv * @internal param Shell $shell */ public function __construct( @@ -53,8 +53,9 @@ public function __construct( /** * Copy Test module files and execute enable module command. * - * @param string $moduleName + * @param string $moduleName * @return string + * @throws LocalizedException */ public function introduceModule($moduleName) { @@ -65,13 +66,14 @@ public function introduceModule($moduleName) /** * Execute enable module command. * - * @param string $moduleName + * @param string $moduleName * @return string + * @throws LocalizedException */ public function enableModule($moduleName) { $initParams = $this->parametersHolder->getInitParams(); - $enableModuleCommand = 'php -f ' . BP . '/bin/magento module:enable ' . $moduleName + $enableModuleCommand = $this->getCliScriptCommand() . ' module:enable ' . $moduleName . ' -n -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; return $this->shell->execute($enableModuleCommand); } @@ -81,11 +83,12 @@ public function enableModule($moduleName) * * @param array $installParams * @return string + * @throws LocalizedException */ public function upgrade($installParams = []) { $initParams = $this->parametersHolder->getInitParams(); - $upgradeCommand = 'php -f ' . BP . '/bin/magento setup:upgrade -vvv -n --magento-init-params="' + $upgradeCommand = $this->getCliScriptCommandWithDI() . 'setup:upgrade -vvv -n --magento-init-params="' . $initParams['magento-init-params'] . '"'; $installParams = $this->toCliArguments($installParams); $upgradeCommand .= ' ' . implode(" ", array_keys($installParams)); @@ -96,13 +99,14 @@ public function upgrade($installParams = []) /** * Execute disable module command. * - * @param string $moduleName + * @param string $moduleName * @return string + * @throws LocalizedException */ public function disableModule($moduleName) { $initParams = $this->parametersHolder->getInitParams(); - $disableModuleCommand = 'php -f ' . BP . '/bin/magento module:disable '. $moduleName + $disableModuleCommand = $this->getCliScriptCommand() . ' module:disable ' . $moduleName . ' -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; return $this->shell->execute($disableModuleCommand); } @@ -111,6 +115,7 @@ public function disableModule($moduleName) * Split quote db configuration. * * @return void + * @throws LocalizedException */ public function splitQuote() { @@ -118,7 +123,7 @@ public function splitQuote() $installParams = $this->toCliArguments( $this->parametersHolder->getDbData('checkout') ); - $command = 'php -f ' . BP . '/bin/magento setup:db-schema:split-quote ' . + $command = $this->getCliScriptCommand() . ' setup:db-schema:split-quote ' . implode(" ", array_keys($installParams)) . ' -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; @@ -130,6 +135,7 @@ public function splitQuote() * Split sales db configuration. * * @return void + * @throws LocalizedException */ public function splitSales() { @@ -137,7 +143,7 @@ public function splitSales() $installParams = $this->toCliArguments( $this->parametersHolder->getDbData('sales') ); - $command = 'php -f ' . BP . '/bin/magento setup:db-schema:split-sales ' . + $command = $this->getCliScriptCommand() . ' setup:db-schema:split-sales ' . implode(" ", array_keys($installParams)) . ' -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; @@ -151,7 +157,7 @@ public function splitSales() public function cacheClean() { $initParams = $this->parametersHolder->getInitParams(); - $command = 'php -f ' . BP . '/bin/magento cache:clean ' . + $command = $this->getCliScriptCommand() . ' cache:clean ' . ' -vvv --magento-init-params=' . $initParams['magento-init-params']; @@ -162,11 +168,12 @@ public function cacheClean() * Uninstall module * * @param string $moduleName + * @throws LocalizedException */ public function uninstallModule($moduleName) { $initParams = $this->parametersHolder->getInitParams(); - $command = 'php -f ' . BP . '/bin/magento module:uninstall ' . $moduleName . ' --remove-data ' . + $command = $this->getCliScriptCommand() . ' module:uninstall ' . $moduleName . ' --remove-data ' . ' -vvv --non-composer --magento-init-params="' . $initParams['magento-init-params'] . '"'; @@ -240,4 +247,29 @@ public function afterInstall() ->get(DeploymentConfig::class); $this->deploymentConfig->resetData(); } + + /** + * Get custom magento-cli command with additional DI configuration + * + * @return string + */ + private function getCliScriptCommandWithDI(): string + { + $params['MAGE_DIRS']['base']['path'] = BP; + $params['INTEGRATION_TESTS_CLI_AUTOLOADER'] = TESTS_BASE_DIR . '/framework/autoload.php'; + $params['TESTS_BASE_DIR'] = TESTS_BASE_DIR; + return 'INTEGRATION_TEST_PARAMS="' . urldecode(http_build_query($params)) . '"' + . ' ' . PHP_BINARY . ' -f ' . INTEGRATION_TESTS_BASE_DIR + . '/bin/magento '; + } + + /** + * Get basic magento-cli command + * + * @return string + */ + private function getCliScriptCommand() + { + return PHP_BINARY . ' -f ' . BP . '/bin/magento '; + } } diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php b/dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php new file mode 100644 index 0000000000000..1a7108150eb24 --- /dev/null +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php @@ -0,0 +1,24 @@ +viewCollectionFactory->create(); $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); diff --git a/lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php b/lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php deleted file mode 100644 index 5991a5f01c398..0000000000000 --- a/lib/internal/Magento/Framework/Mview/TriggerCleanerInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - Config::class, DriverInterface::class => \Magento\Framework\Filesystem\Driver\File::class, ComponentRegistrarInterface::class => ComponentRegistrar::class, - TriggerCleanerInterface::class => TriggerCleaner::class, ], SchemaConfig::class => [ 'parameters' => [ diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index ad94f2e62b819..35d0ac457b31c 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -7,6 +7,7 @@ namespace Magento\Setup\Model; use Magento\Backend\Setup\ConfigOptionsList as BackendConfigOptionsList; +use Magento\Framework\App\Cache\Manager; use Magento\Framework\App\Cache\Type\Block as BlockCache; use Magento\Framework\App\Cache\Type\Layout as LayoutCache; use Magento\Framework\App\DeploymentConfig\Reader; @@ -21,11 +22,13 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Filesystem; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Module\ModuleList\Loader as ModuleLoader; use Magento\Framework\Module\ModuleListInterface; -use Magento\Framework\Mview\TriggerCleanerInterface; +use Magento\Framework\Mview\TriggerCleaner; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\FilePermissions; use Magento\Framework\Setup\InstallDataInterface; @@ -34,20 +37,22 @@ use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\PatchApplier; use Magento\Framework\Setup\Patch\PatchApplierFactory; +use Magento\Framework\Setup\SampleData\State; use Magento\Framework\Setup\SchemaPersistor; use Magento\Framework\Setup\SchemaSetupInterface; use Magento\Framework\Setup\UpgradeDataInterface; use Magento\Framework\Setup\UpgradeSchemaInterface; +use Magento\Framework\Validation\ValidationException; use Magento\PageCache\Model\Cache\Type as PageCache; use Magento\Setup\Console\Command\InstallCommand; use Magento\Setup\Controller\ResponseTypeInterface; +use Magento\Setup\Exception; use Magento\Setup\Model\ConfigModel as SetupConfigModel; use Magento\Setup\Module\ConnectionFactory; use Magento\Setup\Module\DataSetupFactory; use Magento\Setup\Module\SetupFactory; use Magento\Setup\Validator\DbValidator; use Magento\Store\Model\Store; -use Magento\Framework\App\Cache\Manager; /** * Class Installer contains the logic to install Magento application. @@ -217,7 +222,7 @@ class Installer private $dataSetupFactory; /** - * @var \Magento\Framework\Setup\SampleData\State + * @var State */ protected $sampleDataState; @@ -249,7 +254,7 @@ class Installer private $patchApplierFactory; /** - * @var TriggerCleanerInterface + * @var TriggerCleaner */ private $triggerCleaner; @@ -274,10 +279,10 @@ class Installer * @param DbValidator $dbValidator * @param SetupFactory $setupFactory * @param DataSetupFactory $dataSetupFactory - * @param \Magento\Framework\Setup\SampleData\State $sampleDataState + * @param State $sampleDataState * @param ComponentRegistrar $componentRegistrar * @param PhpReadinessCheck $phpReadinessCheck - * @throws \Magento\Setup\Exception + * @throws Exception * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -299,7 +304,7 @@ public function __construct( DbValidator $dbValidator, SetupFactory $setupFactory, DataSetupFactory $dataSetupFactory, - \Magento\Framework\Setup\SampleData\State $sampleDataState, + State $sampleDataState, ComponentRegistrar $componentRegistrar, PhpReadinessCheck $phpReadinessCheck ) { @@ -326,7 +331,7 @@ public function __construct( $this->componentRegistrar = $componentRegistrar; $this->phpReadinessCheck = $phpReadinessCheck; $this->schemaPersistor = $this->objectManagerProvider->get()->get(SchemaPersistor::class); - $this->triggerCleaner = $this->objectManagerProvider->get()->get(TriggerCleanerInterface::class); + $this->triggerCleaner = $this->objectManagerProvider->get()->get(TriggerCleaner::class); } /** @@ -334,7 +339,9 @@ public function __construct( * * @param \ArrayObject|array $request * @return void - * @throws \LogicException + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException */ public function install($request) { @@ -398,6 +405,7 @@ public function install($request) * Get declaration installer. For upgrade process it must be created after deployment config update. * * @return DeclarationInstaller + * @throws Exception */ private function getDeclarationInstaller() { @@ -414,6 +422,7 @@ private function getDeclarationInstaller() * * @return void * @SuppressWarnings(PHPMD.UnusedPrivateMethod) Called by install() via callback. + * @throws FileSystemException */ private function writeInstallationDate() { @@ -429,7 +438,9 @@ private function writeInstallationDate() * @param \ArrayObject|array $request * @param bool $dryRun * @return array - * @throws \LogicException + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException */ private function createModulesConfig($request, $dryRun = false) { @@ -555,6 +566,9 @@ public function checkApplicationFilePermissions() * * @param \ArrayObject|array $data * @return void + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException */ public function installDeploymentConfig($data) { @@ -575,6 +589,7 @@ public function installDeploymentConfig($data) * * @param SchemaSetupInterface $setup * @return void + * @throws \Zend_Db_Exception */ private function setupModuleRegistry(SchemaSetupInterface $setup) { @@ -673,6 +688,7 @@ private function setupSessionTable( * @param SchemaSetupInterface $setup * @param AdapterInterface $connection * @return void + * @throws \Zend_Db_Exception */ private function setupCacheTable( SchemaSetupInterface $setup, @@ -727,6 +743,7 @@ private function setupCacheTable( * @param SchemaSetupInterface $setup * @param AdapterInterface $connection * @return void + * @throws \Zend_Db_Exception */ private function setupCacheTagTable( SchemaSetupInterface $setup, @@ -763,6 +780,7 @@ private function setupCacheTagTable( * @param SchemaSetupInterface $setup * @param AdapterInterface $connection * @return void + * @throws \Zend_Db_Exception */ private function setupFlagTable( SchemaSetupInterface $setup, @@ -819,6 +837,7 @@ private function setupFlagTable( * * @param array $request * @return void + * @throws Exception */ public function declarativeInstallSchema(array $request) { @@ -852,6 +871,9 @@ private function cleanMemoryTables(SchemaSetupInterface $setup) * * @param array $request * @return void + * @throws Exception + * @throws \Magento\Framework\Setup\Exception + * @throws \Zend_Db_Exception */ public function installSchema(array $request) { @@ -898,6 +920,8 @@ private function convertationOfOldScriptsIsAllowed(array $request) * * @param array $request * @return void + * @throws Exception + * @throws \Magento\Framework\Setup\Exception */ public function installDataFixtures(array $request = []) { @@ -963,7 +987,7 @@ private function throwExceptionForNotWritablePaths(array $paths) * @param array $request * @return void * @throws \Magento\Framework\Setup\Exception - * @throws \Magento\Setup\Exception + * @throws Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -971,7 +995,7 @@ private function throwExceptionForNotWritablePaths(array $paths) private function handleDBSchemaData($setup, $type, array $request) { if (!($type === 'schema' || $type === 'data')) { - throw new \Magento\Setup\Exception("Unsupported operation type $type is requested"); + throw new Exception("Unsupported operation type $type is requested"); } $resource = new \Magento\Framework\Module\ModuleResource($this->context); $verType = $type . '-version'; @@ -1085,13 +1109,15 @@ private function handleDBSchemaData($setup, $type, array $request) * Assert DbConfigExists * * @return void - * @throws \Magento\Setup\Exception + * @throws Exception + * @throws FileSystemException + * @throws RuntimeException */ private function assertDbConfigExists() { $config = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT); if (!$config) { - throw new \Magento\Setup\Exception( + throw new Exception( "Can't run this operation: configuration for DB connection is absent." ); } @@ -1114,6 +1140,8 @@ private function isDryRun(array $request) * * @param \ArrayObject|array $data * @return void + * @throws Exception + * @throws LocalizedException */ public function installUserConfig($data) { @@ -1143,8 +1171,8 @@ public function installUserConfig($data) * * @param \ArrayObject|array $data * @return void - * @throws \Magento\Framework\Validation\ValidationException - * @throws \Magento\Setup\Exception + * @throws ValidationException + * @throws Exception */ public function installSearchConfiguration($data) { @@ -1159,13 +1187,13 @@ public function installSearchConfiguration($data) * @param string $className * @param string $interfaceName * @return mixed|null - * @throws \Magento\Setup\Exception + * @throws Exception */ protected function createSchemaDataHandler($className, $interfaceName) { if (class_exists($className)) { if (!is_subclass_of($className, $interfaceName) && $className !== $interfaceName) { - throw new \Magento\Setup\Exception($className . ' must implement \\' . $interfaceName); + throw new Exception($className . ' must implement \\' . $interfaceName); } else { return $this->objectManagerProvider->get()->create($className); } @@ -1222,6 +1250,9 @@ private function installOrderIncrementPrefix($orderIncrementPrefix) * * @param \ArrayObject|array $data * @return void + * @throws Exception + * @throws FileSystemException + * @throws RuntimeException */ public function installAdminUser($data) { @@ -1245,13 +1276,13 @@ public function installAdminUser($data) * * @param bool $keepGeneratedFiles Cleanup generated classes and view files and reset ObjectManager * @return void - * @throws \Magento\Setup\Exception + * @throws Exception */ public function updateModulesSequence($keepGeneratedFiles = false) { $config = $this->deploymentConfig->get(ConfigOptionsListConstants::KEY_MODULES); if (!$config) { - throw new \Magento\Setup\Exception( + throw new Exception( "Can't run this operation: deployment configuration is absent." . " Run 'magento setup:config:set --help' for options." ); @@ -1316,6 +1347,7 @@ public function uninstall() * @param bool $isEnabled * @param array $types * @return void + * @throws Exception */ private function updateCaches($isEnabled, $types = []) { @@ -1350,6 +1382,7 @@ function (string $key) use ($types) { * @return void * * @SuppressWarnings(PHPMD.UnusedPrivateMethod) Called by install() via callback. + * @throws Exception */ private function cleanCaches() { @@ -1425,6 +1458,7 @@ public function cleanupDb() * Removes deployment configuration * * @return void + * @throws FileSystemException */ private function deleteDeploymentConfig() { @@ -1449,6 +1483,9 @@ private function deleteDeploymentConfig() * Validates that MySQL is accessible and MySQL version is supported * * @return void + * @throws Exception + * @throws FileSystemException + * @throws RuntimeException */ private function assertDbAccessible() { @@ -1511,7 +1548,7 @@ private function assertDbAccessible() * @param string $moduleName * @param string $type * @return InstallSchemaInterface | UpgradeSchemaInterface | InstallDataInterface | UpgradeDataInterface | null - * @throws \Magento\Setup\Exception + * @throws Exception */ private function getSchemaDataHandler($moduleName, $type) { @@ -1542,7 +1579,7 @@ private function getSchemaDataHandler($moduleName, $type) $interface = self::DATA_INSTALL; break; default: - throw new \Magento\Setup\Exception("$className does not exist"); + throw new Exception("$className does not exist"); } return $this->createSchemaDataHandler($className, $interface); @@ -1554,7 +1591,7 @@ private function getSchemaDataHandler($moduleName, $type) * @param \Magento\Framework\Module\ModuleResource $resource * @param string $type * @return ModuleContext[] - * @throws \Magento\Setup\Exception + * @throws Exception */ private function generateListOfModuleContext($resource, $type) { @@ -1565,7 +1602,7 @@ private function generateListOfModuleContext($resource, $type) } elseif ($type === 'data-version') { $dbVer = $resource->getDataVersion($moduleName); } else { - throw new \Magento\Setup\Exception("Unsupported version type $type is requested"); + throw new Exception("Unsupported version type $type is requested"); } if ($dbVer !== false) { $moduleContextList[$moduleName] = new ModuleContext($dbVer); @@ -1656,10 +1693,12 @@ private function updateColumnType( /** * Remove unused triggers from db + * + * @throws \Exception */ public function removeUnusedTriggers(): void { - $this->triggerCleaner->unsubscribe(); + $this->triggerCleaner->removeTriggers(); $this->cleanCaches(); } } From dbea58a03f070c4d9ed3bd671a351a42c04b4234 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Wed, 22 Jul 2020 07:12:07 -0500 Subject: [PATCH 21/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- .../Magento/Framework/Mview/TriggerCleaner.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/internal/Magento/Framework/Mview/TriggerCleaner.php b/lib/internal/Magento/Framework/Mview/TriggerCleaner.php index 0c28acd9b9e03..ac2db0a6f4816 100644 --- a/lib/internal/Magento/Framework/Mview/TriggerCleaner.php +++ b/lib/internal/Magento/Framework/Mview/TriggerCleaner.php @@ -54,15 +54,16 @@ public function __construct( */ public function removeTriggers(): bool { + // Get list of views that are enabled $viewCollection = $this->viewCollectionFactory->create(); $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); - // Unsubscribe mviews + // Unsubscribe existing view to remove triggers from db foreach ($viewList as $view) { $view->unsubscribe(); } - // Unsubscribe old views that still have triggers in db + // Remove any remaining triggers from db that are not linked to a view $triggerTableNames = $this->getTableNamesWithTriggers(); foreach ($triggerTableNames as $tableName) { $view = $this->createViewByTableName($tableName); @@ -70,7 +71,7 @@ public function removeTriggers(): bool $view->getState()->delete(); } - // Re-subscribe mviews + // Restore the previous state of the views to add triggers back to db foreach ($viewList as $view) { $view->subscribe(); } @@ -100,6 +101,9 @@ private function getTableNamesWithTriggers(): array /** * Create view by db table name * + * Create a view that has the table name so that unsubscribe can be used to + * remove triggers with the correct naming structure from the db + * * @param string $tableName * @return ViewInterface */ From dd40340d896c26a113b8d4645ef352447f020887 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Wed, 22 Jul 2020 12:48:50 -0500 Subject: [PATCH 22/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- dev/tests/setup-integration/framework/bootstrap.php | 2 ++ .../Framework/Mview/Test/Unit/View/CollectionTest.php | 9 +++++++++ setup/src/Magento/Setup/Model/Installer.php | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/dev/tests/setup-integration/framework/bootstrap.php b/dev/tests/setup-integration/framework/bootstrap.php index f10f430f2f401..20dde0224d73e 100644 --- a/dev/tests/setup-integration/framework/bootstrap.php +++ b/dev/tests/setup-integration/framework/bootstrap.php @@ -102,6 +102,8 @@ /* Unset declared global variables to release the PHPUnit from maintaining their values between tests */ unset($testsBaseDir, $logWriter, $settings, $shell, $application, $bootstrap); } catch (\Exception $e) { + // phpcs:disable Magento2.Security.LanguageConstruct echo $e . PHP_EOL; exit(1); + // phpcs:enable Magento2.Security.LanguageConstruct } diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php index 522aff5d43b7b..0247503093278 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php @@ -143,6 +143,15 @@ function ($elem) { $indexers )); + $this->mviewConfigMock + ->method('getView') + ->willReturnMap(array_map( + function ($elem) { + return [$elem, ['view_id' => $elem]]; + }, + $views + )); + $this->entityFactoryMock ->method('create') ->willReturnMap([ diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index 35d0ac457b31c..d53dede971fa4 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -995,6 +995,7 @@ private function throwExceptionForNotWritablePaths(array $paths) private function handleDBSchemaData($setup, $type, array $request) { if (!($type === 'schema' || $type === 'data')) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new Exception("Unsupported operation type $type is requested"); } $resource = new \Magento\Framework\Module\ModuleResource($this->context); @@ -1117,6 +1118,7 @@ private function assertDbConfigExists() { $config = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT); if (!$config) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new Exception( "Can't run this operation: configuration for DB connection is absent." ); @@ -1193,6 +1195,7 @@ protected function createSchemaDataHandler($className, $interfaceName) { if (class_exists($className)) { if (!is_subclass_of($className, $interfaceName) && $className !== $interfaceName) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new Exception($className . ' must implement \\' . $interfaceName); } else { return $this->objectManagerProvider->get()->create($className); @@ -1282,6 +1285,7 @@ public function updateModulesSequence($keepGeneratedFiles = false) { $config = $this->deploymentConfig->get(ConfigOptionsListConstants::KEY_MODULES); if (!$config) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new Exception( "Can't run this operation: deployment configuration is absent." . " Run 'magento setup:config:set --help' for options." @@ -1579,6 +1583,7 @@ private function getSchemaDataHandler($moduleName, $type) $interface = self::DATA_INSTALL; break; default: + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new Exception("$className does not exist"); } @@ -1602,6 +1607,7 @@ private function generateListOfModuleContext($resource, $type) } elseif ($type === 'data-version') { $dbVer = $resource->getDataVersion($moduleName); } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new Exception("Unsupported version type $type is requested"); } if ($dbVer !== false) { From c172ef3439095dc02862f5da64267154d2d5ec00 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Wed, 22 Jul 2020 16:06:06 -0500 Subject: [PATCH 23/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- dev/tests/setup-integration/framework/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/setup-integration/framework/bootstrap.php b/dev/tests/setup-integration/framework/bootstrap.php index 20dde0224d73e..e3eed312a21b9 100644 --- a/dev/tests/setup-integration/framework/bootstrap.php +++ b/dev/tests/setup-integration/framework/bootstrap.php @@ -100,7 +100,7 @@ ); /* Unset declared global variables to release the PHPUnit from maintaining their values between tests */ - unset($testsBaseDir, $logWriter, $settings, $shell, $application, $bootstrap); + unset($testsBaseDir, $settings, $shell, $application, $bootstrap); } catch (\Exception $e) { // phpcs:disable Magento2.Security.LanguageConstruct echo $e . PHP_EOL; From 2a5ca3609f968d4a8dca6a1ee912c5f32905c3f3 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" Date: Thu, 23 Jul 2020 14:24:24 +0300 Subject: [PATCH 24/65] MC-35989: Custom address attribute is missing on Payment step of checkout --- .../view/frontend/web/js/model/customer/address.js | 4 ++-- .../Customer/frontend/js/model/customer/address.test.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js b/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js index a6d1de5fde255..eba9a8c3ea7ae 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js +++ b/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js @@ -6,7 +6,7 @@ /** * @api */ -define([], function () { +define(['underscore'], function (_) { 'use strict'; /** @@ -44,7 +44,7 @@ define([], function () { vatId: addressData['vat_id'], sameAsBilling: addressData['same_as_billing'], saveInAddressBook: addressData['save_in_address_book'], - customAttributes: addressData['custom_attributes'], + customAttributes: _.toArray(addressData['custom_attributes']).reverse(), /** * @return {*} diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js index fc00772375e3b..45c509a901c47 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js @@ -22,9 +22,12 @@ define([ it('Check on empty object.', function () { var addressData = { region: {} + }, + expected = { + customAttributes: [] }; - expect(JSON.stringify(customerAddress(addressData))).toEqual(JSON.stringify({})); + expect(JSON.stringify(customerAddress(addressData))).toEqual(JSON.stringify(expected)); }); it('Check on function call with empty address data.', function () { @@ -49,7 +52,8 @@ define([ } }), expected = { - regionId: '1' + regionId: '1', + customAttributes: [] }; expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); From e6deb9c3c391d285cccc1c7a7bcc8ddb52f4eb7c Mon Sep 17 00:00:00 2001 From: Max Lesechko Date: Thu, 23 Jul 2020 15:02:44 -0500 Subject: [PATCH 25/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- dev/tests/integration/bin/magento | 6 +++++- .../framework/Magento/TestFramework/Console/CliProxy.php | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/bin/magento b/dev/tests/integration/bin/magento index 4a86eaee01e83..e3c0be6eaffca 100755 --- a/dev/tests/integration/bin/magento +++ b/dev/tests/integration/bin/magento @@ -33,7 +33,11 @@ try { try { $handler = new \Magento\Framework\App\ErrorHandler(); set_error_handler([$handler, 'handler']); - $application = new CliProxy('Magento CLI'); + if ($_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER']) { + $application = new CliProxy('Magento CLI'); + } else { + $application = new Cli('Magento CLI'); + } $application->run(); } catch (\Exception $e) { while ($e) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php index d47931f4734ab..497f234dfa84b 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php @@ -13,6 +13,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Provides the ability to inject additional DI configuration to call a CLI command + */ class CliProxy implements \Magento\Framework\ObjectManager\NoninterceptableInterface { /** From efacaca8074ce3e283e347ad170cfdf2b8a3fcc5 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Fri, 24 Jul 2020 12:34:40 -0500 Subject: [PATCH 26/65] MC-36110: Remove google-shopping-ads clean cache fails --- setup/src/Magento/Setup/Model/Installer.php | 21 +++++++++++++++++++ .../Setup/Test/Unit/Model/InstallerTest.php | 14 +++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index d53dede971fa4..e0f29825585c1 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -9,6 +9,7 @@ use Magento\Backend\Setup\ConfigOptionsList as BackendConfigOptionsList; use Magento\Framework\App\Cache\Manager; use Magento\Framework\App\Cache\Type\Block as BlockCache; +use Magento\Framework\App\Cache\Type\Config as ConfigCache; use Magento\Framework\App\Cache\Type\Layout as LayoutCache; use Magento\Framework\App\DeploymentConfig\Reader; use Magento\Framework\App\DeploymentConfig\Writer; @@ -1291,6 +1292,7 @@ public function updateModulesSequence($keepGeneratedFiles = false) . " Run 'magento setup:config:set --help' for options." ); } + $this->flushCaches([ConfigCache::TYPE_IDENTIFIER]); $this->cleanCaches(); if (!$keepGeneratedFiles) { $this->cleanupGeneratedFiles(); @@ -1397,6 +1399,25 @@ private function cleanCaches() $this->log->log('Cache cleared successfully'); } + /** + * Flush caches for specific types or all available types + * + * @param array $types + * @return void + * + * @throws Exception + */ + private function flushCaches($types = []) + { + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->get(Manager::class); + if (empty($types)) { + $types = $cacheManager->getAvailableTypes(); + } + $cacheManager->flush($types); + $this->log->log('Cache types ' . implode(',', $types) . ' flushed successfully'); + } + /** * Enables or disables maintenance mode for Magento application * diff --git a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php index 8446486c2f104..48afa684bb9d2 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php @@ -588,11 +588,12 @@ public function testUpdateModulesSequence() ); $installer = $this->prepareForUpdateModulesTests(); - $this->logger->expects($this->at(0))->method('log')->with('Cache cleared successfully'); - $this->logger->expects($this->at(1))->method('log')->with('File system cleanup:'); - $this->logger->expects($this->at(2))->method('log') + $this->logger->expects($this->at(0))->method('log')->with('Cache types config flushed successfully'); + $this->logger->expects($this->at(1))->method('log')->with('Cache cleared successfully'); + $this->logger->expects($this->at(2))->method('log')->with('File system cleanup:'); + $this->logger->expects($this->at(3))->method('log') ->with('The directory \'/generation\' doesn\'t exist - skipping cleanup'); - $this->logger->expects($this->at(3))->method('log')->with('Updating modules:'); + $this->logger->expects($this->at(4))->method('log')->with('Updating modules:'); $installer->updateModulesSequence(false); } @@ -602,8 +603,9 @@ public function testUpdateModulesSequenceKeepGenerated() $installer = $this->prepareForUpdateModulesTests(); - $this->logger->expects($this->at(0))->method('log')->with('Cache cleared successfully'); - $this->logger->expects($this->at(1))->method('log')->with('Updating modules:'); + $this->logger->expects($this->at(0))->method('log')->with('Cache types config flushed successfully'); + $this->logger->expects($this->at(1))->method('log')->with('Cache cleared successfully'); + $this->logger->expects($this->at(2))->method('log')->with('Updating modules:'); $installer->updateModulesSequence(true); } From 7d0579d4e9dd700c92347854b38f828eeb0eb76a Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Fri, 24 Jul 2020 15:10:24 -0500 Subject: [PATCH 27/65] MC-36110: Remove google-shopping-ads clean cache fails --- setup/src/Magento/Setup/Model/Installer.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index e0f29825585c1..296782c3873c0 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -1411,9 +1411,7 @@ private function flushCaches($types = []) { /** @var Manager $cacheManager */ $cacheManager = $this->objectManagerProvider->get()->get(Manager::class); - if (empty($types)) { - $types = $cacheManager->getAvailableTypes(); - } + $types = empty($types) ? $cacheManager->getAvailableTypes() : $types; $cacheManager->flush($types); $this->log->log('Cache types ' . implode(',', $types) . ' flushed successfully'); } From 4faf6460297f24b880dbab24ec9298b26c0ee5a6 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 27 Jul 2020 10:04:58 +0300 Subject: [PATCH 28/65] MC-35026: When a Downloadable Product in a Group Product The Order does not show links --- .../Observer/SaveDownloadableOrderItemObserver.php | 14 ++++++++------ .../SaveDownloadableOrderItemObserverTest.php | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 4f7939da478fa..341387d3317c7 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Observer; use Magento\Framework\Event\ObserverInterface; @@ -81,12 +83,14 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderItem = $observer->getEvent()->getItem(); if (!$orderItem->getId()) { //order not saved in the database return $this; } - if ($orderItem->getProductType() != \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { + $productType = $orderItem->getRealProductType() ?: $orderItem->getProductType(); + if ($productType != \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { return $this; } $product = $orderItem->getProduct(); @@ -112,13 +116,13 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($linkIds = $orderItem->getProductOptionByCode('links')) { $linkPurchased = $this->_createPurchasedModel(); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_order::class, + 'downloadable_sales_copy_order', 'to_downloadable', $orderItem->getOrder(), $linkPurchased ); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_order_item::class, + 'downloadable_sales_copy_order_item', 'to_downloadable', $orderItem, $linkPurchased @@ -131,14 +135,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) ScopeInterface::SCOPE_STORE ); $linkPurchased->setLinkSectionTitle($linkSectionTitle)->save(); - $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING; if ($orderStatusToEnableItem == \Magento\Sales\Model\Order\Item::STATUS_PENDING || $orderItem->getOrder()->getState() == \Magento\Sales\Model\Order::STATE_COMPLETE ) { $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE; } - foreach ($linkIds as $linkId) { if (isset($links[$linkId])) { $linkPurchasedItem = $this->_createPurchasedItemModel()->setPurchasedId( @@ -148,7 +150,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_link::class, + 'downloadable_sales_copy_link', 'to_purchased', $links[$linkId], $linkPurchasedItem diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php index 80f23c859a031..09edbf4935fe4 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php @@ -176,6 +176,9 @@ public function testSaveDownloadableOrderItem() $itemMock->expects($this->any()) ->method('getProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); + $itemMock->expects($this->any()) + ->method('getRealProductType') + ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); $this->orderMock->expects($this->once()) ->method('getStoreId') @@ -311,6 +314,9 @@ public function testSaveDownloadableOrderItemSavedPurchasedLink() $itemMock->expects($this->any()) ->method('getProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); + $itemMock->expects($this->any()) + ->method('getRealProductType') + ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); $purchasedLink = $this->getMockBuilder(Purchased::class) ->disableOriginalConstructor() From 2dbab41631cce46be4a27dcbf10a7b172be61f63 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" Date: Mon, 27 Jul 2020 11:38:25 +0300 Subject: [PATCH 29/65] MC-35675: Orders with Zero Payment Information required are Closed after being invoiced --- .../Model/Order/Invoice/Total/Discount.php | 45 +++-- .../Order/Invoice/Total/DiscountTest.php | 159 ++++++++++++++++++ 2 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php index acd0d0c67d8c0..ef7205b374415 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php @@ -5,13 +5,20 @@ */ namespace Magento\Sales\Model\Order\Invoice\Total; +use Magento\Sales\Model\Order\Invoice; + +/** + * Discount invoice + */ class Discount extends AbstractTotal { /** - * @param \Magento\Sales\Model\Order\Invoice $invoice + * Collect invoice + * + * @param Invoice $invoice * @return $this */ - public function collect(\Magento\Sales\Model\Order\Invoice $invoice) + public function collect(Invoice $invoice) { $invoice->setDiscountAmount(0); $invoice->setBaseDiscountAmount(0); @@ -24,14 +31,7 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) * So basically if we have invoice with positive discount and it * was not canceled we don't add shipping discount to this one. */ - $addShippingDiscount = true; - foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { - if ($previousInvoice->getDiscountAmount()) { - $addShippingDiscount = false; - } - } - - if ($addShippingDiscount) { + if ($this->isShippingDiscount($invoice)) { $totalDiscountAmount = $totalDiscountAmount + $invoice->getOrder()->getShippingDiscountAmount(); $baseTotalDiscountAmount = $baseTotalDiscountAmount + $invoice->getOrder()->getBaseShippingDiscountAmount(); @@ -71,8 +71,29 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $invoice->setDiscountAmount(-$totalDiscountAmount); $invoice->setBaseDiscountAmount(-$baseTotalDiscountAmount); - $invoice->setGrandTotal($invoice->getGrandTotal() - $totalDiscountAmount); - $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() - $baseTotalDiscountAmount); + $grandTotal = $invoice->getGrandTotal() - $totalDiscountAmount < 0.0001 + ? 0 : $invoice->getGrandTotal() - $totalDiscountAmount; + $baseGrandTotal = $invoice->getBaseGrandTotal() - $baseTotalDiscountAmount < 0.0001 + ? 0 : $invoice->getBaseGrandTotal() - $baseTotalDiscountAmount; + $invoice->setGrandTotal($grandTotal); + $invoice->setBaseGrandTotal($baseGrandTotal); return $this; } + + /** + * Checking if shipping discount was added in previous invoices. + * + * @param Invoice $invoice + * @return bool + */ + private function isShippingDiscount(Invoice $invoice): bool + { + $addShippingDiscount = true; + foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { + if ($previousInvoice->getDiscountAmount()) { + $addShippingDiscount = false; + } + } + return $addShippingDiscount; + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php new file mode 100644 index 0000000000000..f7587031337a7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php @@ -0,0 +1,159 @@ +objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject(Discount::class); + $this->order = $this->createPartialMock(Order::class, [ + 'getInvoiceCollection', + ]); + $this->invoice = $this->createPartialMock(Invoice::class, [ + 'getAllItems', + 'getOrder', + 'roundPrice', + 'isLast', + 'getGrandTotal', + 'getBaseGrandTotal', + 'setGrandTotal', + 'setBaseGrandTotal' + ]); + } + + /** + * Test for collect invoice + * + * @param array $invoiceData + * @dataProvider collectInvoiceData + * @return void + */ + public function testCollectInvoiceWithZeroGrandTotal(array $invoiceData): void + { + //Set up invoice mock + /** @var InvoiceItem[] $invoiceItems */ + $invoiceItems = []; + foreach ($invoiceData as $invoiceItemData) { + $invoiceItems[] = $this->getInvoiceItem($invoiceItemData); + } + $this->invoice->method('getOrder') + ->willReturn($this->order); + $this->order->method('getInvoiceCollection') + ->willReturn([]); + $this->invoice->method('getAllItems') + ->willReturn($invoiceItems); + $this->invoice->method('getGrandTotal') + ->willReturn(15.6801); + $this->invoice->method('getBaseGrandTotal') + ->willReturn(15.6801); + + $this->invoice->expects($this->exactly(1)) + ->method('setGrandTotal') + ->with(0); + $this->invoice->expects($this->exactly(1)) + ->method('setBaseGrandTotal') + ->with(0); + $this->model->collect($this->invoice); + } + + /** + * @return array + */ + public function collectInvoiceData(): array + { + return [ + [ + [ + [ + 'order_item' => [ + 'qty_ordered' => 1, + 'discount_amount' => 5.34, + 'base_discount_amount' => 5.34, + ], + 'is_last' => true, + 'qty' => 1, + ], + [ + 'order_item' => [ + 'qty_ordered' => 1, + 'discount_amount' => 10.34, + 'base_discount_amount' => 10.34, + ], + 'is_last' => true, + 'qty' => 1, + ], + ], + ], + ]; + } + + /** + * Get InvoiceItem + * + * @param $invoiceItemData array + * @return InvoiceItem|MockObject + */ + protected function getInvoiceItem($invoiceItemData) + { + /** @var OrderItem|MockObject $orderItem */ + $orderItem = $this->createPartialMock(OrderItem::class, [ + 'isDummy', + ]); + foreach ($invoiceItemData['order_item'] as $key => $value) { + $orderItem->setData($key, $value); + } + /** @var InvoiceItem|MockObject $invoiceItem */ + $invoiceItem = $this->createPartialMock(InvoiceItem::class, [ + 'getOrderItem', + 'isLast', + ]); + $invoiceItem->method('getOrderItem') + ->willReturn($orderItem); + $invoiceItem->method('isLast') + ->willReturn($invoiceItemData['is_last']); + $invoiceItem->getData('qty', $invoiceItemData['qty']); + return $invoiceItem; + } +} From 06efa819a905f0307c6d42dbd50d770b82f9e175 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 27 Jul 2020 12:13:13 +0300 Subject: [PATCH 30/65] MC-35479: Unable to download import history file --- .../Magento/ImportExport/Model/Report/Csv.php | 38 +++--- .../Test/Unit/Model/Report/CsvTest.php | 124 ------------------ .../ImportExport/Model/Report/CsvTest.php | 90 +++++++++++++ 3 files changed, 105 insertions(+), 147 deletions(-) delete mode 100644 app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php create mode 100644 dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php diff --git a/app/code/Magento/ImportExport/Model/Report/Csv.php b/app/code/Magento/ImportExport/Model/Report/Csv.php index 7279092265cbb..e7ddef1008444 100644 --- a/app/code/Magento/ImportExport/Model/Report/Csv.php +++ b/app/code/Magento/ImportExport/Model/Report/Csv.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ImportExport\Model\Report; @@ -60,22 +61,16 @@ public function __construct( } /** - * @param string $originalFileName - * @param ProcessingErrorAggregatorInterface $errorAggregator - * @param bool $writeOnlyErrorItems - * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @inheritDoc */ public function createReport( $originalFileName, ProcessingErrorAggregatorInterface $errorAggregator, $writeOnlyErrorItems = false ) { - $sourceCsv = $this->createSourceCsvModel($originalFileName); - - $outputFileName = $this->generateOutputFileName($originalFileName); - $outputCsv = $this->createOutputCsvModel($outputFileName); + $outputCsv = $this->outputCsvFactory->create(); + $sourceCsv = $this->createSourceCsvModel($originalFileName); $columnsName = $sourceCsv->getColNames(); $columnsName[] = self::REPORT_ERROR_COLUMN_NAME; $outputCsv->setHeaderCols($columnsName); @@ -88,10 +83,16 @@ public function createReport( } } + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $outputFileName = $this->generateOutputFileName($originalFileName); + $directory->writeFile(Import::IMPORT_HISTORY_DIR . $outputFileName, $outputCsv->getContents()); + return $outputFileName; } /** + * Retrieve error messages + * * @param int $rowNumber * @param ProcessingErrorAggregatorInterface $errorAggregator * @return string @@ -112,16 +113,21 @@ public function retrieveErrorMessagesByRowNumber($rowNumber, ProcessingErrorAggr } /** + * Generate output filename based on source filename + * * @param string $sourceFile * @return string */ protected function generateOutputFileName($sourceFile) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileName = basename($sourceFile, self::ERROR_REPORT_FILE_EXTENSION); return $fileName . self::ERROR_REPORT_FILE_SUFFIX . self::ERROR_REPORT_FILE_EXTENSION; } /** + * Create source CSV model + * * @param string $sourceFile * @return \Magento\ImportExport\Model\Import\Source\Csv */ @@ -135,18 +141,4 @@ protected function createSourceCsvModel($sourceFile) ] ); } - - /** - * @param string $outputFileName - * @return \Magento\ImportExport\Model\Export\Adapter\Csv - */ - protected function createOutputCsvModel($outputFileName) - { - return $this->outputCsvFactory->create( - [ - 'destination' => Import::IMPORT_HISTORY_DIR . $outputFileName, - 'destinationDirectoryCode' => DirectoryList::VAR_DIR, - ] - ); - } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php deleted file mode 100644 index 9ca7f2bcbc9c7..0000000000000 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php +++ /dev/null @@ -1,124 +0,0 @@ -reportHelperMock = $this->createMock(Report::class); - $this->reportHelperMock->expects($this->any())->method('getDelimiter')->willReturn($testDelimiter); - - $this->outputCsvFactoryMock = $this->createPartialMock( - CsvFactory::class, - ['create'] - ); - $this->outputCsvMock = $this->createMock(Csv::class); - $this->outputCsvFactoryMock->expects($this->any())->method('create')->willReturn($this->outputCsvMock); - - $this->sourceCsvFactoryMock = $this->createPartialMock( - \Magento\ImportExport\Model\Import\Source\CsvFactory::class, - ['create'] - ); - $this->sourceCsvMock = $this->createMock(\Magento\ImportExport\Model\Import\Source\Csv::class); - $this->sourceCsvMock->expects($this->any())->method('valid')->willReturnOnConsecutiveCalls(true, true, false); - $this->sourceCsvMock->expects($this->any())->method('current')->willReturnOnConsecutiveCalls( - [23 => 'first error'], - [27 => 'second error'] - ); - $this->sourceCsvFactoryMock - ->expects($this->any()) - ->method('create') - ->with( - [ - 'file' => 'some_file_name', - 'directory' => null, - 'delimiter' => $testDelimiter - ] - ) - ->willReturn($this->sourceCsvMock); - - $this->filesystemMock = $this->createMock(Filesystem::class); - - $this->csvModel = $objectManager->getObject( - \Magento\ImportExport\Model\Report\Csv::class, - [ - 'reportHelper' => $this->reportHelperMock, - 'sourceCsvFactory' => $this->sourceCsvFactoryMock, - 'outputCsvFactory' => $this->outputCsvFactoryMock, - 'filesystem' => $this->filesystemMock - ] - ); - } - - public function testCreateReport() - { - $errorAggregatorMock = $this->createMock( - ProcessingErrorAggregator::class - ); - $errorProcessingMock = $this->createPartialMock( - ProcessingError::class, - ['getErrorMessage'] - ); - $errorProcessingMock->expects($this->any())->method('getErrorMessage')->willReturn('some_error_message'); - $errorAggregatorMock->expects($this->any())->method('getErrorByRowNumber')->willReturn([$errorProcessingMock]); - $this->sourceCsvMock->expects($this->any())->method('getColNames')->willReturn([]); - - $name = $this->csvModel->createReport('some_file_name', $errorAggregatorMock, true); - - $this->assertEquals($name, 'some_file_name_error_report.csv'); - } -} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php new file mode 100644 index 0000000000000..60f50b15a281b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php @@ -0,0 +1,90 @@ +create(Filesystem::class); + $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + + $this->csvReport = Bootstrap::getObjectManager()->create(Csv::class); + } + /** + * @inheritDoc + */ + protected function tearDown() + { + foreach ([$this->importFilePath, $this->reportPath] as $path) { + if ($path && $this->directory->isExist($path)) { + $this->directory->delete($path); + } + } + } + + /** + * @return void + */ + public function testCreateReport() + { + $importData = <<importFilePath = 'test_import.csv'; + $this->directory->writeFile($this->importFilePath, $importData); + + $errorAggregator = Bootstrap::getObjectManager()->create(ProcessingErrorAggregatorInterface::class); + $error = 'Value for \'weight\' attribute contains incorrect value'; + $errorAggregator->addError($error, ProcessingError::ERROR_LEVEL_CRITICAL, 1, 'weight', $error); + + $outputFileName = $this->csvReport->createReport( + $this->directory->getAbsolutePath($this->importFilePath), + $errorAggregator + ); + + $this->reportPath = Import::IMPORT_HISTORY_DIR . $outputFileName; + $this->assertTrue($this->directory->isExist($this->reportPath), 'Report was not generated'); + } +} From 2f496e293418708f751189b36e803fa1d02503ef Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 27 Jul 2020 14:28:37 +0300 Subject: [PATCH 31/65] MC-35479: Unable to download import history file --- .../testsuite/Magento/ImportExport/Model/Report/CsvTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php index 60f50b15a281b..218221c35632c 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php @@ -44,7 +44,7 @@ class CsvTest extends \PHPUnit\Framework\TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); @@ -54,7 +54,7 @@ protected function setUp() /** * @inheritDoc */ - protected function tearDown() + protected function tearDown(): void { foreach ([$this->importFilePath, $this->reportPath] as $path) { if ($path && $this->directory->isExist($path)) { From b3222cdbb867a1cd1a35bf169cb4c9f2f808cf8f Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Fri, 24 Jul 2020 15:23:19 +0300 Subject: [PATCH 32/65] MC-32271: Order of State/Province drop down is not consistent in admin UI --- .../view/frontend/web/js/region-updater.js | 15 ++++-- lib/web/mage/adminhtml/form.js | 48 ++++++++++++++----- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index 6d54f607484b4..d9de1e0935faa 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -157,7 +157,10 @@ define([ regionInput = $(this.options.regionInputId), postcode = $(this.options.postcodeId), label = regionList.parent().siblings('label'), - container = regionList.parents('div.field'); + container = regionList.parents('div.field'), + regionsEntries, + regionId, + regionData; this._clearError(); this._checkRegionRequired(country); @@ -168,8 +171,14 @@ define([ // Populate state/province dropdown list if available or use input box if (this.options.regionJson[country]) { this._removeSelectOptions(regionList); - $.each(this.options.regionJson[country], $.proxy(function (key, value) { - this._renderSelectOption(regionList, key, value); + regionsEntries = Object.entries(this.options.regionJson[country]); + regionsEntries.sort(function (a, b) { + return a[1].name > b[1].name ? 1 : -1; + }); + $.each(regionsEntries, $.proxy(function (key, value) { + regionId = value[0]; + regionData = value[1]; + this._renderSelectOption(regionList, regionId, regionData); }, this)); if (this.currentRegionOption) { diff --git a/lib/web/mage/adminhtml/form.js b/lib/web/mage/adminhtml/form.js index 4dfbde6afa9d7..eae359c4b26a4 100644 --- a/lib/web/mage/adminhtml/form.js +++ b/lib/web/mage/adminhtml/form.js @@ -133,6 +133,7 @@ define([ this.config = regions.config; delete regions.config; this.regions = regions; + this.sortedRegions = this.getSortedRegions(); this.disableAction = typeof disableAction === 'undefined' ? 'hide' : disableAction; this.clearRegionValueOnDisable = typeof clearRegionValueOnDisable === 'undefined' ? false : clearRegionValueOnDisable; @@ -246,11 +247,13 @@ define([ * Update. */ update: function () { - var option, region, def, regionId; + var option, selectElement, def, regionId, region; - if (this.regions[this.countryEl.value]) { + selectElement = this.regionSelectEl; + + if (this.sortedRegions[this.countryEl.value]) { if (this.lastCountryId != this.countryEl.value) { //eslint-disable-line eqeqeq - def = this.regionSelectEl.getAttribute('defaultValue'); + def = selectElement.getAttribute('defaultValue'); if (this.regionTextEl) { if (!def) { @@ -259,26 +262,27 @@ define([ this.regionTextEl.value = ''; } - this.regionSelectEl.options.length = 1; + selectElement.options.length = 1; - for (regionId in this.regions[this.countryEl.value]) { //eslint-disable-line guard-for-in - region = this.regions[this.countryEl.value][regionId]; + this.sortedRegions[this.countryEl.value].forEach(function (item) { + regionId = item[0]; + region = item[1]; option = document.createElement('OPTION'); option.value = regionId; option.text = region.name.stripTags(); option.title = region.name; - if (this.regionSelectEl.options.add) { - this.regionSelectEl.options.add(option); + if (selectElement.options.add) { + selectElement.options.add(option); } else { - this.regionSelectEl.appendChild(option); + selectElement.appendChild(option); } if (regionId == def || region.name.toLowerCase() == def || region.code.toLowerCase() == def) { //eslint-disable-line - this.regionSelectEl.value = regionId; + selectElement.value = regionId; } - } + }); } if (this.disableAction == 'hide') { //eslint-disable-line eqeqeq @@ -340,6 +344,28 @@ define([ display ? marks[0].show() : marks[0].hide(); } } + }, + + /** + * Sort regions from JSON by name + * + * @returns {*[]} + */ + getSortedRegions: function () { + var country, regionsEntries, regionsByCountry; + + regionsByCountry = []; + + for (country in this.regions) { //eslint-disable-line guard-for-in + regionsEntries = Object.entries(this.regions[country]); + regionsEntries.sort(function (a, b) { + return a[1].name > b[1].name ? 1 : -1; + }); + + regionsByCountry[country] = regionsEntries; + } + + return regionsByCountry; } }; From 78ef828f723825960281828a67a8f0562f973f54 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 27 Jul 2020 14:35:44 +0300 Subject: [PATCH 33/65] MC-36060: [On Premise] - State field is disabled after country change --- .../view/frontend/web/js/region-updater.js | 4 +- .../frontend/js/region-updater.test.js | 206 ++++++++++++++++++ 2 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index d9de1e0935faa..2555aa876b71e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -171,7 +171,7 @@ define([ // Populate state/province dropdown list if available or use input box if (this.options.regionJson[country]) { this._removeSelectOptions(regionList); - regionsEntries = Object.entries(this.options.regionJson[country]); + regionsEntries = _.pairs(this.options.regionJson[country]); regionsEntries.sort(function (a, b) { return a[1].name > b[1].name ? 1 : -1; }); @@ -202,7 +202,7 @@ define([ regionList.hide(); container.hide(); } else { - regionList.show(); + regionList.removeAttr('disabled').show(); } } diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js new file mode 100644 index 0000000000000..6b77c35b9fe60 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js @@ -0,0 +1,206 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*eslint-disable max-nested-callbacks*/ +/*jscs:disable jsDoc*/ +define([ + 'jquery', + 'Magento_Checkout/js/region-updater' +], function ($) { + 'use strict'; + + var regionJson = { + 'config': { + 'show_all_regions': true, + 'regions_required': [ + 'US' + ] + }, + 'US': { + '1': { + 'code': 'AL', + 'name': 'Alabama' + }, + '2': { + 'code': 'AK', + 'name': 'Alaska' + }, + '3': { + 'code': 'AS', + 'name': 'American Samoa' + } + }, + 'DE': { + '81': { + 'code': 'BAY', + 'name': 'Bayern' + }, + '82': { + 'code': 'BER', + 'name': 'Berlin' + }, + '83': { + 'code': 'BRG', + 'name': 'Brandenburg' + } + } + }, + defaultCountry = 'GB', + countries = { + '': '', + 'US': 'United States', + 'GB': 'United Kingdom', + 'DE': 'Germany' + }, + regions = { + '': 'Please select a region, state or province.' + }, + countryEl, + regionSelectEl, + regionInputEl, + postalCodeEl, + formEl, + containerEl; + + function createFormField() { + var fieldWrapperEl = document.createElement('div'), + labelEl = document.createElement('label'), + inputControlEl = document.createElement('div'), + i; + + fieldWrapperEl.appendChild(labelEl); + fieldWrapperEl.appendChild(inputControlEl); + + for (i = 0; i < arguments.length; i++) { + inputControlEl.appendChild(arguments[i]); + } + labelEl.setAttribute('class', 'label'); + fieldWrapperEl.setAttribute('class', 'field required'); + inputControlEl.setAttribute('class', 'control'); + + return fieldWrapperEl; + } + + function buildSelectOptions(select, options, defaultOption) { + var optionValue, + optionEl; + + defaultOption = typeof defaultOption === 'undefined' ? '' : defaultOption; + + // eslint-disable-next-line guard-for-in + for (optionValue in options) { + if (options.hasOwnProperty(optionValue)) { + optionEl = document.createElement('option'); + optionEl.setAttribute('value', optionValue); + optionEl.textContent = countries[optionValue]; + // eslint-disable-next-line max-depth + if (defaultOption === optionValue) { + optionEl.setAttribute('selected', 'selected'); + } + select.add(optionEl); + } + } + } + + function init(config) { + var defaultConfig = { + 'optionalRegionAllowed': true, + 'regionListId': '#' + regionSelectEl.id, + 'regionInputId': '#' + regionInputEl.id, + 'postcodeId': '#' + postalCodeEl.id, + 'form': '#' + formEl.id, + 'regionJson': regionJson, + 'defaultRegion': 0, + 'countriesWithOptionalZip': ['GB'] + }; + + $(countryEl).regionUpdater($.extend({}, defaultConfig, config || {})); + } + + beforeEach(function () { + containerEl = document.createElement('div'); + formEl = document.createElement('form'); + regionSelectEl = document.createElement('select'); + regionInputEl = document.createElement('input'); + postalCodeEl = document.createElement('input'); + countryEl = document.createElement('select'); + regionSelectEl.setAttribute('id', 'region_id'); + regionSelectEl.setAttribute('style', 'display:none;'); + regionInputEl.setAttribute('id', 'region'); + regionInputEl.setAttribute('style', 'display:none;'); + countryEl.setAttribute('id', 'country'); + postalCodeEl.setAttribute('id', 'zip'); + formEl.setAttribute('id', 'test_form'); + formEl.appendChild(createFormField(countryEl)); + formEl.appendChild(createFormField(regionSelectEl, regionInputEl)); + formEl.appendChild(createFormField(postalCodeEl)); + containerEl.appendChild(formEl); + buildSelectOptions(regionSelectEl, regions); + buildSelectOptions(countryEl, countries, defaultCountry); + document.body.appendChild(containerEl); + }); + + afterEach(function () { + $(containerEl).remove(); + formEl = undefined; + containerEl = undefined; + regionSelectEl = undefined; + regionInputEl = undefined; + postalCodeEl = undefined; + countryEl = undefined; + }); + + describe('Magento_Checkout/js/region-updater', function () { + it('Check that default country is selected', function () { + init(); + expect($(countryEl).val()).toBe(defaultCountry); + }); + it('Check that region list is not displayed when selected country has no predefined regions', function () { + init(); + $(countryEl).val('GB').change(); + expect($(regionInputEl).is(':visible')).toBe(true); + expect($(regionInputEl).is(':disabled')).toBe(false); + expect($(regionSelectEl).is(':visible')).toBe(false); + expect($(regionSelectEl).is(':disabled')).toBe(true); + }); + it('Check country that has predefined and optional regions', function () { + init(); + $(countryEl).val('DE').change(); + expect($(regionSelectEl).is(':visible')).toBe(true); + expect($(regionSelectEl).is(':disabled')).toBe(false); + expect($(regionSelectEl).hasClass('required-entry')).toBe(false); + expect($(regionInputEl).is(':visible')).toBe(false); + expect( + $(regionSelectEl).find('option') + .map(function () { + return this.textContent; + }) + .get() + ).toContain('Berlin'); + }); + it('Check country that has predefined and required regions', function () { + init(); + $(countryEl).val('US').change(); + expect($(regionSelectEl).is(':visible')).toBe(true); + expect($(regionSelectEl).is(':disabled')).toBe(false); + expect($(regionSelectEl).hasClass('required-entry')).toBe(true); + expect($(regionInputEl).is(':visible')).toBe(false); + expect( + $(regionSelectEl).find('option') + .map(function () { + return this.textContent; + }) + .get() + ).toContain('Alaska'); + }); + it('Check that region fields are not displayed for country with optional regions if configured', function () { + init({ + optionalRegionAllowed: false + }); + $(countryEl).val('DE').change(); + expect($(regionSelectEl).is(':visible')).toBe(false); + expect($(regionInputEl).is(':visible')).toBe(false); + }); + }); +}); From aa68df53db28b40eb23dd0ae78e91d8d87ecc796 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 27 Jul 2020 16:38:48 +0300 Subject: [PATCH 34/65] MC-35026: When a Downloadable Product in a Group Product The Order does not show links --- .../Downloadable/Observer/SaveDownloadableOrderItemObserver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 341387d3317c7..9351568c5a757 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -90,7 +90,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) return $this; } $productType = $orderItem->getRealProductType() ?: $orderItem->getProductType(); - if ($productType != \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { + if ($productType !== \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { return $this; } $product = $orderItem->getProduct(); From b8760b32ddcd491bb154fd6d63e02d5848a715b2 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Mon, 27 Jul 2020 14:55:27 -0500 Subject: [PATCH 35/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 --- dev/tests/integration/bin/magento | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/bin/magento b/dev/tests/integration/bin/magento index e3c0be6eaffca..8226f5c711708 100755 --- a/dev/tests/integration/bin/magento +++ b/dev/tests/integration/bin/magento @@ -33,7 +33,7 @@ try { try { $handler = new \Magento\Framework\App\ErrorHandler(); set_error_handler([$handler, 'handler']); - if ($_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER']) { + if (isset($_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER'])) { $application = new CliProxy('Magento CLI'); } else { $application = new Cli('Magento CLI'); From 8339eae08d4a86b7d26bd390f0d5a6ad8d1feec6 Mon Sep 17 00:00:00 2001 From: Krissy Hiserote Date: Mon, 27 Jul 2020 17:13:32 -0500 Subject: [PATCH 36/65] MC-32014: Remove google-shopping-ads module from core in 2.4.1 - fix merge conflict --- .../Magento/Setup/Console/Command/UpgradeCommand.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index af25466fa141d..10a2ffa05a796 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -11,6 +11,7 @@ use Magento\Framework\App\State as AppState; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Config\CacheInterface; use Magento\Framework\Setup\ConsoleLogger; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\Declaration\Schema\OperationsExecutor; @@ -54,22 +55,30 @@ class UpgradeCommand extends AbstractSetupCommand */ private $searchConfigFactory; + /* + * @var CacheInterface + */ + private $cache; + /** * @param InstallerFactory $installerFactory * @param SearchConfigFactory $searchConfigFactory * @param DeploymentConfig $deploymentConfig * @param AppState|null $appState + * @param CacheInterface|null $cache */ public function __construct( InstallerFactory $installerFactory, SearchConfigFactory $searchConfigFactory, DeploymentConfig $deploymentConfig = null, - AppState $appState = null + AppState $appState = null, + CacheInterface $cache = null ) { $this->installerFactory = $installerFactory; $this->searchConfigFactory = $searchConfigFactory; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->appState = $appState ?: ObjectManager::getInstance()->get(AppState::class); + $this->cache = $cache ?: ObjectManager::getInstance()->get(CacheInterface::class); parent::__construct(); } @@ -131,6 +140,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $installer = $this->installerFactory->create(new ConsoleLogger($output)); $installer->updateModulesSequence($keepGenerated); $searchConfig = $this->searchConfigFactory->create(); + $this->cache->clean(); $searchConfig->validateSearchEngine(); $installer->removeUnusedTriggers(); $installer->installSchema($request); From 605d847e997a8ab9177b0cd48474984f56706628 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Mon, 27 Jul 2020 20:21:52 -0400 Subject: [PATCH 37/65] Added a filter to sort categories by position by default --- .../Magento/CatalogGraphQl/Model/Category/CategoryFilter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index 1fae247c981d2..dc93005983776 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Sort; use Magento\Search\Model\Query; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\ScopeInterface; @@ -71,6 +72,7 @@ public function getResult(array $criteria, StoreInterface $store) $categoryIds = []; $criteria[Filter::ARGUMENT_NAME] = $this->formatMatchFilters($criteria['filters'], $store); $criteria[Filter::ARGUMENT_NAME][CategoryInterface::KEY_IS_ACTIVE] = ['eq' => 1]; + $criteria[Sort::ARGUMENT_NAME][CategoryInterface::KEY_POSITION] = ['ASC']; $searchCriteria = $this->searchCriteriaBuilder->build('categoryList', $criteria); $pageSize = $criteria['pageSize'] ?? 20; $currentPage = $criteria['currentPage'] ?? 1; From 8109a5403675b03542f756c239d12cf4e44e07e6 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Tue, 28 Jul 2020 13:59:01 +0300 Subject: [PATCH 38/65] MC-35658: Checkout summary section is flickering on scrolling of checkout page --- lib/web/mage/sticky.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/web/mage/sticky.js b/lib/web/mage/sticky.js index b6e29bb3cae20..78ff63168b9ba 100644 --- a/lib/web/mage/sticky.js +++ b/lib/web/mage/sticky.js @@ -68,6 +68,9 @@ define([ this.element.on('dimensionsChanged', $.proxy(this.reset, this)); this.reset(); + + // Application of the workaround for IE11 and Edge + this.normalizeIE11AndEdgeScroll(); }, /** @@ -128,11 +131,27 @@ define([ }, /** - * Facade method that palces sticky element where it should be. + * Facade method that places sticky element where it should be. */ reset: function () { this._calculateDimens() ._stick(); + }, + + /** + * Workaround for IE11 and Edge that solves the IE known rendering issue + * that prevents sticky element from jumpy movement on scrolling the page. + * + * Alternatively, undesired jumpy movement can be eliminated by changing the setting in IE: + * Settings > Internet options > Advanced tab > inside 'Browsing' item > set 'Use smooth scrolling' to False + */ + normalizeIE11AndEdgeScroll: function () { + if (navigator.userAgent.match(/Trident.*rv[ :]*11\.|Edge\//)) { + document.body.addEventListener('mousewheel', function () { + event.preventDefault(); + window.scrollTo(0, window.pageYOffset - event.wheelDelta); + }); + } } }); From 262445fbc6f635e8a5e5c3b4ecabb93411f93a4d Mon Sep 17 00:00:00 2001 From: dnyomo Date: Tue, 28 Jul 2020 07:36:52 -0400 Subject: [PATCH 39/65] Test Fixes for the new filter that sorts categories by position --- .../CategoriesQuery/CategoriesFilterTest.php | 18 +++++++++--------- .../CategoriesPaginationTest.php | 2 +- .../GraphQl/Catalog/CategoryListTest.php | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 4da588794b2a9..4444b81619bd3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -648,15 +648,6 @@ public function filterMultipleCategoriesDataProvider(): array 'in', '["category-1-2", "movable"]', [ - [ - 'id' => '7', - 'name' => 'Movable', - 'url_key' => 'movable', - 'url_path' => 'movable', - 'children_count' => '0', - 'path' => '1/2/7', - 'position' => '3' - ], [ 'id' => '13', 'name' => 'Category 1.2', @@ -665,6 +656,15 @@ public function filterMultipleCategoriesDataProvider(): array 'children_count' => '0', 'path' => '1/2/3/13', 'position' => '2' + ], + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' ] ] ], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php index c7fbcbd38c7e4..bbc84a82737bd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php @@ -155,7 +155,7 @@ public function testPaging() $lastPageQuery = sprintf($baseQuery, $page1Result['categories']['page_info']['total_pages']); $lastPageResult = $this->graphQlQuery($lastPageQuery); $this->assertCount(1, $lastPageResult['categories']['items']); - $this->assertEquals('Category 1.2', $lastPageResult['categories']['items'][0]['name']); + $this->assertEquals('Category 12', $lastPageResult['categories']['items'][0]['name']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index d6477c82513e9..c49baf7333dde 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -629,15 +629,6 @@ public function filterMultipleCategoriesDataProvider(): array 'in', '["category-1-2", "movable"]', [ - [ - 'id' => '7', - 'name' => 'Movable', - 'url_key' => 'movable', - 'url_path' => 'movable', - 'children_count' => '0', - 'path' => '1/2/7', - 'position' => '3' - ], [ 'id' => '13', 'name' => 'Category 1.2', @@ -646,6 +637,15 @@ public function filterMultipleCategoriesDataProvider(): array 'children_count' => '0', 'path' => '1/2/3/13', 'position' => '2' + ], + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' ] ] ], From 4c9adb5d653cd296c160f65e25ea0922f73f9ce2 Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Wed, 29 Jul 2020 15:29:39 -0500 Subject: [PATCH 40/65] MC-35903: Refactor place where lock mechanism is used regarding proper lock description --- .../Console/StartConsumerCommand.php | 32 +++++++++---------- .../Framework/Lock/Backend/CacheTest.php | 29 ++++++----------- .../Cache/LockGuardedCacheLoader.php | 2 +- .../Magento/Framework/Lock/Backend/Cache.php | 28 ++++++++++++---- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php index fc2207dcd7c86..f0f9cf4b68bdb 100644 --- a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php +++ b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php @@ -79,22 +79,20 @@ protected function execute(InputInterface $input, OutputInterface $output) $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); - if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore - $output->writeln('Consumer with the same name is running'); - return \Magento\Framework\Console\Cli::RETURN_FAILURE; - } - - if ($singleThread) { - $this->lockManager->lock(md5($consumerName)); //phpcs:ignore - } - - $this->appState->setAreaCode($areaCode ?? 'global'); - - $consumer = $this->consumerFactory->get($consumerName, $batchSize); - $consumer->process($numberOfMessages); - - if ($singleThread) { - $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore + try { + if ($singleThread && !$this->lockManager->lock(md5($consumerName),0)) { //phpcs:ignore + $output->writeln('Consumer with the same name is running'); + return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + + $this->appState->setAreaCode($areaCode ?? 'global'); + + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); + } finally { + if ($singleThread) { + $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore + } } return \Magento\Framework\Console\Cli::RETURN_SUCCESS; @@ -163,7 +161,7 @@ protected function configure() To specify the preferred area: %command.full_name% someConsumer --area-code='adminhtml' - + To do not run multiple copies of one consumer simultaneously: %command.full_name% someConsumer --single-thread' diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php index 306bda462820a..bf5b282c805e6 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php @@ -47,16 +47,10 @@ public function testParallelLock(): void { $identifier1 = \uniqid('lock_name_1_', true); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 2)); + $this->assertTrue($this->cacheInstance1->lock($identifier1)); - $this->assertFalse($this->cacheInstance1->lock($identifier1, 2)); - $this->assertFalse($this->cacheInstance2->lock($identifier1, 2)); - sleep(4); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); - - $this->assertTrue($this->cacheInstance2->lock($identifier1, -1)); - sleep(4); - $this->assertTrue($this->cacheInstance1->isLocked($identifier1)); + $this->assertFalse($this->cacheInstance1->lock($identifier1, 0)); + $this->assertFalse($this->cacheInstance2->lock($identifier1, 0)); } /** @@ -66,19 +60,16 @@ public function testParallelLock(): void */ public function testParallelLockExpired(): void { - $identifier1 = \uniqid('lock_name_1_', true); + $lifeTime = \Closure::bind(function (Cache $class) { + return $class->defaultLifetime; + }, null, $this->cacheInstance1)($this->cacheInstance1); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $identifier1 = \uniqid('lock_name_1_', true); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $this->assertTrue($this->cacheInstance1->lock($identifier1, 0)); + $this->assertTrue($this->cacheInstance2->lock($identifier1, $lifeTime + 1)); - $this->assertTrue($this->cacheInstance2->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $this->cacheInstance2->unlock($identifier1); } /** diff --git a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php index 439648b3cc32b..d3b4c8852267a 100644 --- a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php +++ b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php @@ -131,7 +131,7 @@ public function lockedLoadData( return $dataCollector(); } - if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + if ($this->locker->lock($lockName, 0)) { try { $data = $dataCollector(); $dataSaver($data); diff --git a/lib/internal/Magento/Framework/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php index 612d8541281b0..c8517c9cf73c4 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Cache.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -31,6 +31,20 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ private $lockSign; + /** + * How many microseconds to wait before re-try to acquire a lock + * + * @var int + */ + private $sleepCycle = 100000; + + /** + * Lifetime of lock data in seconds. + * + * @var int + */ + private $defaultLifetime = 10; + /** * @param FrontendInterface $cache */ @@ -49,14 +63,16 @@ public function lock(string $name, int $timeout = -1): bool $this->lockSign = $this->generateLockSign(); } - $data = $this->cache->load($this->getIdentifier($name)); - - if (false !== $data) { - return false; + $skipDeadline = $timeout < 0; + $deadline = microtime(true) + $timeout; + while ($this->cache->load($this->getIdentifier($name))) { + if (!$skipDeadline && $deadline <= microtime(true)) { + return false; + } + usleep($this->sleepCycle); } - $timeout = $timeout <= 0 ? null : $timeout; - $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); + $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $this->defaultLifetime); $data = $this->cache->load($this->getIdentifier($name)); From 3df1b6197805cf4d8c450af4f2dd4cf5bea0c8c7 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Thu, 30 Jul 2020 15:17:39 -0400 Subject: [PATCH 41/65] Added a new default sort by ID when searching Elasticsearch in SearchCriteriaBuilder.php Remove the _id from the order that is being passed to the product collection in ProductSearch.php Updated the position to be DESC passed to the product collection in ProductSearch.php Updated the ProductSearchTest.php to have the items in the attended order --- .../Product/SearchCriteriaBuilder.php | 20 +++++++++++++++++++ .../Products/DataProvider/ProductSearch.php | 9 ++++++++- .../GraphQl/Catalog/ProductSearchTest.php | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 1057d21283ea8..a91910f3d578a 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -104,6 +104,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte $this->addDefaultSortOrder($searchCriteria, $isSearch); } + $this->addEntityIdSort($searchCriteria, $isSearch); $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); $searchCriteria->setCurrentPage($args['currentPage']); @@ -132,6 +133,25 @@ private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bo $this->addFilter($searchCriteria, 'visibility', $visibilityIds, 'in'); } + /** + * Add sort by Entity ID + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + */ + private function addEntityIdSort(SearchCriteriaInterface $searchCriteria, bool $isSearch): void + { + if ($isSearch) { + return; + } + $sortOrderArray = $searchCriteria->getSortOrders(); + $sortOrderArray[] = $this->sortOrderBuilder + ->setField('_id') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + $searchCriteria->setSortOrders($sortOrderArray); + } + /** * Prepare price aggregation algorithm * diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index c35caa07b4785..45721eff12b48 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; +use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -18,6 +19,7 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; +use Magento\Framework\Api\SortOrder; use Magento\GraphQl\Model\Query\ContextInterface; /** @@ -153,7 +155,12 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) $sortOrders = $searchCriteria->getSortOrders(); if (is_array($sortOrders)) { foreach ($sortOrders as $sortOrder) { - $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); + if ($sortOrder->getField() !== '_id') { + if ($sortOrder->getField() == EavAttributeInterface::POSITION) { + $sortOrder->setDirection(SortOrder::SORT_DESC); + } + $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); + } } } 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 dd5b5827c8017..315b0b51d22bf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -898,7 +898,7 @@ public function testFilterByMultipleProductUrlKeys() $product1 = $productRepository->get('simple'); $product2 = $productRepository->get('12345'); $product3 = $productRepository->get('simple-4'); - $filteredProducts = [$product1, $product2, $product3]; + $filteredProducts = [$product3, $product2, $product1]; $urlKey =[]; foreach ($filteredProducts as $product) { $urlKey[] = $product->getUrlKey(); From 62ba8aa8833444aaf130fd861a66f9b881c1a171 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Thu, 30 Jul 2020 15:41:30 -0400 Subject: [PATCH 42/65] Added a new default sort by ID when searching Elasticsearch in SearchCriteriaBuilder.php Remove the _id from the order that is being passed to the product collection in ProductSearch.php Updated the position to be DESC passed to the product collection in ProductSearch.php Updated the ProductSearchTest.php to have the items in the attended order --- .../Products/DataProvider/ProductSearch.php | 13 ++++++++++--- .../Model/Resolver/Products/Query/Search.php | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 45721eff12b48..afc5fc77bc46d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -86,6 +86,7 @@ public function __construct( * * @param SearchCriteriaInterface $searchCriteria * @param SearchResultInterface $searchResult + * @param array $args * @param array $attributes * @param ContextInterface|null $context * @return SearchResultsInterface @@ -93,6 +94,7 @@ public function __construct( public function getList( SearchCriteriaInterface $searchCriteria, SearchResultInterface $searchResult, + array $args, array $attributes = [], ContextInterface $context = null ): SearchResultsInterface { @@ -105,7 +107,7 @@ public function getList( $this->getSearchResultsApplier( $searchResult, $collection, - $this->getSortOrderArray($searchCriteriaForCollection) + $this->getSortOrderArray($searchCriteriaForCollection, $args) )->apply(); $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); @@ -147,9 +149,10 @@ private function getSearchResultsApplier( * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...] * * @param SearchCriteriaInterface $searchCriteria + * @param array $args * @return array */ - private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) + private function getSortOrderArray(SearchCriteriaInterface $searchCriteria, $args) { $ordersArray = []; $sortOrders = $searchCriteria->getSortOrders(); @@ -157,7 +160,11 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) foreach ($sortOrders as $sortOrder) { if ($sortOrder->getField() !== '_id') { if ($sortOrder->getField() == EavAttributeInterface::POSITION) { - $sortOrder->setDirection(SortOrder::SORT_DESC); + if (isset($args['sort'][EavAttributeInterface::POSITION])) { + $sortOrder->setDirection($args['sort'][EavAttributeInterface::POSITION]); + } else { + $sortOrder->setDirection(SortOrder::SORT_DESC); + } } $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 29c3ce279e6a1..faf1d56452a24 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -103,7 +103,7 @@ public function getResult( //Address limitations of sort and pagination on search API apply original pagination from GQL query $searchCriteria->setPageSize($realPageSize); $searchCriteria->setCurrentPage($realCurrentPage); - $searchResults = $this->productsProvider->getList($searchCriteria, $itemsResults, $queryFields, $context); + $searchResults = $this->productsProvider->getList($searchCriteria, $itemsResults, $args, $queryFields, $context); $totalPages = $realPageSize ? ((int)ceil($searchResults->getTotalCount() / $realPageSize)) : 0; From 565f63ee540d6c6d3ced666c1dc816315e26d54f Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Thu, 30 Jul 2020 16:53:49 -0500 Subject: [PATCH 43/65] MC-35903: Refactor place where lock mechanism is used regarding proper lock description --- .../Unit/Console/StartConsumerCommandTest.php | 23 +++++-------- .../Framework/Lock/Backend/CacheTest.php | 7 ++-- .../Test/Unit/LockGuardedCacheLoaderTest.php | 6 ++-- .../Magento/Framework/Lock/Backend/Cache.php | 33 ++++++++++++++++++- .../Lock/Test/Unit/Backend/CacheTest.php | 2 +- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php index b73fcc278f970..274386a9bb685 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php @@ -103,7 +103,6 @@ public function testExecute( $pidFilePath, $singleThread, $lockExpects, - $isLockedExpects, $isLocked, $unlockExpects, $runProcessExpects, @@ -144,14 +143,11 @@ public function testExecute( ->method('get')->with($consumerName, $batchSize)->willReturn($consumer); $consumer->expects($this->exactly($runProcessExpects))->method('process')->with($numberOfMessages); - $this->lockManagerMock->expects($this->exactly($isLockedExpects)) - ->method('isLocked') - ->with(md5($consumerName)) //phpcs:ignore - ->willReturn($isLocked); - $this->lockManagerMock->expects($this->exactly($lockExpects)) ->method('lock') - ->with(md5($consumerName)); //phpcs:ignore + ->with(md5($consumerName))//phpcs:ignore + ->willReturn($isLocked); + $this->lockManagerMock->expects($this->exactly($unlockExpects)) ->method('unlock') ->with(md5($consumerName)); //phpcs:ignore @@ -172,8 +168,7 @@ public function executeDataProvider() 'pidFilePath' => null, 'singleThread' => false, 'lockExpects' => 0, - 'isLockedExpects' => 0, - 'isLocked' => false, + 'isLocked' => true, 'unlockExpects' => 0, 'runProcessExpects' => 1, 'expectedReturn' => Cli::RETURN_SUCCESS, @@ -182,8 +177,7 @@ public function executeDataProvider() 'pidFilePath' => '/var/consumer.pid', 'singleThread' => true, 'lockExpects' => 1, - 'isLockedExpects' => 1, - 'isLocked' => false, + 'isLocked' => true, 'unlockExpects' => 1, 'runProcessExpects' => 1, 'expectedReturn' => Cli::RETURN_SUCCESS, @@ -191,10 +185,9 @@ public function executeDataProvider() [ 'pidFilePath' => '/var/consumer.pid', 'singleThread' => true, - 'lockExpects' => 0, - 'isLockedExpects' => 1, - 'isLocked' => true, - 'unlockExpects' => 0, + 'lockExpects' => 1, + 'isLocked' => false, + 'unlockExpects' => 1, 'runProcessExpects' => 0, 'expectedReturn' => Cli::RETURN_FAILURE, ], diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php index bf5b282c805e6..81ab34fae9b98 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php @@ -60,14 +60,15 @@ public function testParallelLock(): void */ public function testParallelLockExpired(): void { - $lifeTime = \Closure::bind(function (Cache $class) { - return $class->defaultLifetime; + $testLifeTime = 2; + \Closure::bind(function (Cache $class) use ($testLifeTime) { + $class->defaultLifetime = $testLifeTime; }, null, $this->cacheInstance1)($this->cacheInstance1); $identifier1 = \uniqid('lock_name_1_', true); $this->assertTrue($this->cacheInstance1->lock($identifier1, 0)); - $this->assertTrue($this->cacheInstance2->lock($identifier1, $lifeTime + 1)); + $this->assertTrue($this->cacheInstance2->lock($identifier1, $testLifeTime + 1)); $this->cacheInstance2->unlock($identifier1); } diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php index aa3df00953fda..9dcfb89373c2b 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php @@ -95,7 +95,7 @@ public function testDataCollectedAfterDeadlineReached(): void $this->lockManagerInterfaceMock ->expects($this->atLeastOnce())->method('lock') - ->with($lockName, 10) + ->with($lockName, 0) ->willReturn(false); $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); @@ -129,7 +129,7 @@ public function testDataWrite(): void $this->lockManagerInterfaceMock ->expects($this->once())->method('lock') - ->with($lockName, 10) + ->with($lockName, 0) ->willReturn(true); $this->lockManagerInterfaceMock->expects($this->once())->method('unlock'); @@ -168,7 +168,7 @@ public function testDataCollectedWithParallelGeneration(): void $this->lockManagerInterfaceMock ->expects($this->once())->method('lock') - ->with($lockName, 10) + ->with($lockName, 0) ->willReturn(false); $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); diff --git a/lib/internal/Magento/Framework/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php index c8517c9cf73c4..ae777a6701cde 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Cache.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -43,7 +43,14 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface * * @var int */ - private $defaultLifetime = 10; + private $defaultLifetime = 7200; + + /** + * Array for keeping all lock attempt to release them on destruct. + * + * @var string[] + */ + private $lockArrayState = []; /** * @param FrontendInterface $cache @@ -77,6 +84,7 @@ public function lock(string $name, int $timeout = -1): bool $data = $this->cache->load($this->getIdentifier($name)); if ($data === $this->lockSign) { + $this->lockArrayState[$name] = 1; return true; } @@ -101,6 +109,7 @@ public function unlock(string $name): bool $removeResult = false; if ($data === $this->lockSign) { $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); + unset($this->lockArrayState[$name]); } return $removeResult; @@ -147,4 +156,26 @@ private function generateLockSign() return $sign; } + + /** + * Destruct method should release all locks that left. + * + * @return void + */ + public function __destruct() + { + $this->releaseLocks(); + } + + /** + * Release all locks that were not removed with unlock method. + * + * @return void + */ + private function releaseLocks() + { + foreach ($this->lockArrayState as $name => $value) { + $this->unlock($name); + } + } } diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php index 5b5c87ce454b3..3e46d4fe6fc76 100644 --- a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php @@ -162,6 +162,6 @@ public function testLockWithNotEmptyData(): void ->with(self::LOCK_PREFIX . $identifier) ->willReturn(\uniqid('some_rand-', true)); - $this->assertEquals(false, $this->cache->lock($identifier)); + $this->assertEquals(false, $this->cache->lock($identifier, 0)); } } From 3c9ed6e3352eec373455d5f93f655f28a5d6b4d9 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Fri, 31 Jul 2020 12:03:50 +0300 Subject: [PATCH 44/65] MC-36231: setup:static-content:deploy exits with success exit code even if not all themes are deployed --- app/code/Magento/Deploy/Process/Queue.php | 48 ++++++++------ .../Deploy/Process/TimeoutException.php | 15 +++++ .../Command/DeployStaticContentCommand.php | 63 ++++++++++++------- .../DeployStaticContentCommandTest.php | 44 +++++++++++-- 4 files changed, 124 insertions(+), 46 deletions(-) create mode 100644 app/code/Magento/Deploy/Process/TimeoutException.php diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php index 6c8db345187cc..35d85c390b9c4 100644 --- a/app/code/Magento/Deploy/Process/Queue.php +++ b/app/code/Magento/Deploy/Process/Queue.php @@ -29,7 +29,7 @@ class Queue /** * Default max execution time */ - const DEFAULT_MAX_EXEC_TIME = 400; + const DEFAULT_MAX_EXEC_TIME = 900; /** * @var array @@ -96,6 +96,11 @@ class Queue */ private $lastJobStarted = 0; + /** + * @var int + */ + private $logDelay; + /** * @param AppState $appState * @param LocaleResolver $localeResolver @@ -157,11 +162,12 @@ public function getPackages() * Process jobs * * @return int + * @throws TimeoutException */ public function process() { $returnStatus = 0; - $logDelay = 10; + $this->logDelay = 10; $this->start = $this->lastJobStarted = time(); $packages = $this->packages; while (count($packages) && $this->checkTimeout()) { @@ -170,13 +176,7 @@ public function process() $this->assertAndExecute($name, $packages, $packageJob); } - // refresh current status in console once in 10 iterations (once in 5 sec) - if ($logDelay >= 10) { - $this->logger->info('.'); - $logDelay = 0; - } else { - $logDelay++; - } + $this->refreshStatus(); if ($this->isCanBeParalleled()) { // in parallel mode sleep before trying to check status and run new jobs @@ -193,9 +193,28 @@ public function process() $this->awaitForAllProcesses(); + if (!empty($packages)) { + throw new TimeoutException('Not all packages are deployed.'); + } + return $returnStatus; } + /** + * Refresh current status in console once in 10 iterations (once in 5 sec) + * + * @return void + */ + private function refreshStatus(): void + { + if ($this->logDelay >= 10) { + $this->logger->info('.'); + $this->logDelay = 0; + } else { + $this->logDelay++; + } + } + /** * Check that all depended packages deployed and execute * @@ -204,7 +223,7 @@ public function process() * @param array $packageJob * @return void */ - private function assertAndExecute($name, array & $packages, array $packageJob) + private function assertAndExecute($name, array &$packages, array $packageJob) { /** @var Package $package */ $package = $packageJob['package']; @@ -256,7 +275,6 @@ private function executePackage(Package $package, string $name, array &$packages */ private function awaitForAllProcesses() { - $logDelay = 10; while ($this->inProgress && $this->checkTimeout()) { foreach ($this->inProgress as $name => $package) { if ($this->isDeployed($package)) { @@ -264,13 +282,7 @@ private function awaitForAllProcesses() } } - // refresh current status in console once in 10 iterations (once in 5 sec) - if ($logDelay >= 10) { - $this->logger->info('.'); - $logDelay = 0; - } else { - $logDelay++; - } + $this->refreshStatus(); // sleep before checking parallel jobs status // phpcs:ignore Magento2.Functions.DiscouragedFunction diff --git a/app/code/Magento/Deploy/Process/TimeoutException.php b/app/code/Magento/Deploy/Process/TimeoutException.php new file mode 100644 index 0000000000000..2d8eb3ab2aad2 --- /dev/null +++ b/app/code/Magento/Deploy/Process/TimeoutException.php @@ -0,0 +1,15 @@ +getOption(Options::FORCE_RUN) && $this->getAppState()->getMode() !== State::MODE_PRODUCTION) { - throw new LocalizedException( - __( - 'NOTE: Manual static content deployment is not required in "default" and "developer" modes.' - . PHP_EOL . 'In "default" and "developer" modes static contents are being deployed ' - . 'automatically on demand.' - . PHP_EOL . 'If you still want to deploy in these modes, use -f option: ' - . "'bin/magento setup:static-content:deploy -f'" - ) - ); - } - + $this->checkAppMode($input); $this->inputValidator->validate($input); $options = $input->getOptions(); @@ -136,18 +127,44 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->mockCache(); - /** @var DeployStaticContent $deployService */ - $deployService = $this->objectManager->create(DeployStaticContent::class, [ - 'logger' => $logger - ]); - - $deployService->deploy($options); + $exitCode = Cli::RETURN_SUCCESS; + try { + /** @var DeployStaticContent $deployService */ + $deployService = $this->objectManager->create(DeployStaticContent::class, [ + 'logger' => $logger + ]); + $deployService->deploy($options); + } catch (\Throwable $e) { + $logger->error('Error happened during deploy process: ' . $e->getMessage()); + $exitCode = Cli::RETURN_FAILURE; + } if (!$refreshOnly) { $logger->notice(PHP_EOL . "Execution time: " . (microtime(true) - $time)); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + return $exitCode; + } + + /** + * Check application mode + * + * @param InputInterface $input + * @throws LocalizedException + */ + private function checkAppMode(InputInterface $input): void + { + if (!$input->getOption(Options::FORCE_RUN) && $this->getAppState()->getMode() !== State::MODE_PRODUCTION) { + throw new LocalizedException( + __( + 'NOTE: Manual static content deployment is not required in "default" and "developer" modes.' + . PHP_EOL . 'In "default" and "developer" modes static contents are being deployed ' + . 'automatically on demand.' + . PHP_EOL . 'If you still want to deploy in these modes, use -f option: ' + . "'bin/magento setup:static-content:deploy -f'" + ) + ); + } } /** diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php b/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php index f01c99ccc336f..f205f255abb2a 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php @@ -10,21 +10,24 @@ use Magento\Deploy\Console\ConsoleLogger; use Magento\Deploy\Console\ConsoleLoggerFactory; use Magento\Deploy\Console\DeployStaticOptions; - use Magento\Deploy\Console\InputValidator; +use Magento\Deploy\Process\TimeoutException; use Magento\Deploy\Service\DeployStaticContent; use Magento\Framework\App\State; +use Magento\Framework\Console\Cli; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\Setup\Console\Command\DeployStaticContentCommand; use Magento\Setup\Model\ObjectManagerProvider; use PHPUnit\Framework\MockObject\MockObject as Mock; - use PHPUnit\Framework\TestCase; - use Symfony\Component\Console\Tester\CommandTester; +/** + * Test for static content deploy command + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class DeployStaticContentCommandTest extends TestCase { /** @@ -111,7 +114,8 @@ public function testExecute($input) $this->deployService->expects($this->once())->method('deploy'); $tester = new CommandTester($this->command); - $tester->execute($input); + $exitCode = $tester->execute($input); + $this->assertEquals(Cli::RETURN_SUCCESS, $exitCode); } /** @@ -129,6 +133,36 @@ public function executeDataProvider() ]; } + /** + * @return void + */ + public function testExecuteWithError() + { + $this->appState->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_PRODUCTION); + + $this->inputValidator->expects($this->once()) + ->method('validate'); + + $this->consoleLoggerFactory->expects($this->once()) + ->method('getLogger') + ->willReturn($this->logger); + $this->logger->expects($this->once()) + ->method('error'); + + $this->objectManager->expects($this->once()) + ->method('create') + ->willReturn($this->deployService); + $this->deployService->expects($this->once()) + ->method('deploy') + ->willThrowException(new TimeoutException()); + + $tester = new CommandTester($this->command); + $exitCode = $tester->execute([]); + $this->assertEquals(Cli::RETURN_FAILURE, $exitCode); + } + /** * @param string $mode * @return void From 340001779400e91f82c58ef2e34f6cc55b79e292 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Fri, 31 Jul 2020 10:21:23 -0400 Subject: [PATCH 45/65] Updated tests to cover changes to the sort orders --- .../Catalog/CategoriesQuery/CategoriesFilterTest.php | 8 ++++---- .../GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php | 4 ++-- .../Magento/GraphQl/Catalog/CategoryListTest.php | 8 ++++---- .../Magento/GraphQl/Catalog/ProductSearchTest.php | 2 +- .../testsuite/Magento/GraphQl/Catalog/ProductViewTest.php | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 4444b81619bd3..e8246ac36f406 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -187,9 +187,9 @@ public function testQueryChildCategoriesWithProducts() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'] + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children @@ -280,9 +280,9 @@ public function testQueryCategoryWithDisabledChildren() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'] + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children 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 c2e82e734cd9b..ad9ffa9eb7edd 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 @@ -441,7 +441,7 @@ public function testCategoryProducts() $this->assertArrayHasKey('products', $response['categories']['items'][0]); $baseCategory = $response['categories']['items'][0]; $this->assertArrayHasKey('total_count', $baseCategory['products']); - $this->assertGreaterThanOrEqual(1, $baseCategory['products']['total_count']); + //$this->assertGreaterThanOrEqual(1, $baseCategory['products']['total_count']); $this->assertEquals(1, $baseCategory['products']['page_info']['current_page']); $this->assertEquals(20, $baseCategory['products']['page_info']['page_size']); $this->assertArrayHasKey('sku', $baseCategory['products']['items'][0]); @@ -453,7 +453,7 @@ public function testCategoryProducts() $this->assertAttributes($firstProduct); $this->assertWebsites($firstProductModel, $firstProduct['websites']); $this->assertEquals('Category 1', $firstProduct['categories'][0]['name']); - $this->assertEquals('category-1/category-1-1', $firstProduct['categories'][1]['url_path']); + $this->assertEquals('movable-position-2', $firstProduct['categories'][1]['url_path']); $this->assertCount(3, $firstProduct['categories']); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index c49baf7333dde..3dcba3277dfa7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -186,9 +186,9 @@ public function testQueryChildCategoriesWithProducts() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'] + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children @@ -277,9 +277,9 @@ public function testQueryCategoryWithDisabledChildren() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'] + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children 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 315b0b51d22bf..226f240a247ec 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -313,7 +313,7 @@ public function testFilterProductsByDropDownCustomAttribute() $product1 = $productRepository->get('simple'); $product2 = $productRepository->get('12345'); $product3 = $productRepository->get('simple-4'); - $filteredProducts = [$product1, $product2, $product3 ]; + $filteredProducts = [$product3, $product2, $product1]; $countOfFilteredProducts = count($filteredProducts); $this->reIndexAndCleanCache(); $response = $this->graphQlQuery($query); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index c6719f1862ddc..87f8b62ed84a1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -232,7 +232,7 @@ public function testQueryAllFieldsSimpleProduct() special_from_date special_price special_to_date - swatch_image + swatch_image tier_price tier_prices { @@ -578,8 +578,8 @@ public function testProductLinks() */ public function testProductPrices() { - $firstProductSku = 'simple-249'; - $secondProductSku = 'simple-156'; + $firstProductSku = 'simple-156'; + $secondProductSku = 'simple-249'; $query = << Date: Fri, 31 Jul 2020 13:12:57 -0400 Subject: [PATCH 46/65] Remove the position from the filter and replaced it with Entity Id --- .../Product/SearchCriteriaBuilder.php | 28 +++++++++++++------ .../Products/DataProvider/ProductSearch.php | 20 +++++++------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index a91910f3d578a..6a686cf301fcc 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -101,7 +101,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } if (!$searchCriteria->getSortOrders()) { - $this->addDefaultSortOrder($searchCriteria, $isSearch); + $this->addDefaultSortOrder($searchCriteria, $args, $isSearch); } $this->addEntityIdSort($searchCriteria, $isSearch); @@ -199,18 +199,28 @@ private function addFilter( * Sort by relevance DESC by default * * @param SearchCriteriaInterface $searchCriteria + * @param array $args * @param bool $isSearch */ - private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, array $args, $isSearch = false): void { - $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION; - $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC; - $defaultSortOrder = $this->sortOrderBuilder - ->setField($sortField) - ->setDirection($sortDirection) - ->create(); + $defaultSortOrder = []; + if ($isSearch) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField('relevance') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + } else { + $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; + if ($categoryIdFilter && count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField(EavAttributeInterface::POSITION) + ->setDirection(SortOrder::SORT_ASC) + ->create(); + } + } - $searchCriteria->setSortOrders([$defaultSortOrder]); + $searchCriteria->setSortOrders($defaultSortOrder); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index afc5fc77bc46d..e51c7c60648ac 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -20,6 +20,7 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; use Magento\Framework\Api\SortOrder; +use Magento\Framework\Exception\InputException; use Magento\GraphQl\Model\Query\ContextInterface; /** @@ -151,6 +152,7 @@ private function getSearchResultsApplier( * @param SearchCriteriaInterface $searchCriteria * @param array $args * @return array + * @throws InputException */ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria, $args) { @@ -158,16 +160,18 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria, $arg $sortOrders = $searchCriteria->getSortOrders(); if (is_array($sortOrders)) { foreach ($sortOrders as $sortOrder) { - if ($sortOrder->getField() !== '_id') { - if ($sortOrder->getField() == EavAttributeInterface::POSITION) { - if (isset($args['sort'][EavAttributeInterface::POSITION])) { - $sortOrder->setDirection($args['sort'][EavAttributeInterface::POSITION]); - } else { - $sortOrder->setDirection(SortOrder::SORT_DESC); - } + if ($sortOrder->getField() === '_id') { + $sortOrder->setField('entity_id'); + $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; + if ($categoryIdFilter && count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1) { + $sortOrder->setDirection(SortOrder::SORT_ASC); } - $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); } + $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); + } + if (isset($ordersArray[EavAttributeInterface::POSITION])) { + $ordersArray['entity_id'] = $ordersArray[EavAttributeInterface::POSITION]; + unset($ordersArray[EavAttributeInterface::POSITION]); } } From 30c79f336b42cb381ed57a6c23654b469e261313 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Fri, 31 Jul 2020 13:48:15 -0400 Subject: [PATCH 47/65] Removed bad code that was not needed --- .../Resolver/Products/DataProvider/ProductSearch.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index e51c7c60648ac..e4823b4aa557a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -162,17 +162,9 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria, $arg foreach ($sortOrders as $sortOrder) { if ($sortOrder->getField() === '_id') { $sortOrder->setField('entity_id'); - $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; - if ($categoryIdFilter && count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1) { - $sortOrder->setDirection(SortOrder::SORT_ASC); - } } $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); } - if (isset($ordersArray[EavAttributeInterface::POSITION])) { - $ordersArray['entity_id'] = $ordersArray[EavAttributeInterface::POSITION]; - unset($ordersArray[EavAttributeInterface::POSITION]); - } } return $ordersArray; From c137e0ec7cd852e5a8ed66e685cd9613504d01f0 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Sun, 2 Aug 2020 09:59:22 -0400 Subject: [PATCH 48/65] Updated the default sort to check if the category id is passed as an array. Reverted the tests back to check on products --- .../DataProvider/Product/SearchCriteriaBuilder.php | 5 ++++- .../GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php | 4 ++-- .../GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php | 2 +- .../testsuite/Magento/GraphQl/Catalog/CategoryListTest.php | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 6a686cf301fcc..173d055180119 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -212,7 +212,10 @@ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, ar ->create(); } else { $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; - if ($categoryIdFilter && count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1) { + if ($categoryIdFilter && + !is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) || + count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1 + ) { $defaultSortOrder[] = $this->sortOrderBuilder ->setField(EavAttributeInterface::POSITION) ->setDirection(SortOrder::SORT_ASC) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index e8246ac36f406..3ba7805ba2d25 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -280,9 +280,9 @@ public function testQueryCategoryWithDisabledChildren() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'], - ['sku' => 'simple', 'name' => 'Simple Product'] + ['sku' => '12345', 'name' => 'Simple Product Two'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children 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 ad9ffa9eb7edd..d4089161cd894 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 @@ -441,7 +441,7 @@ public function testCategoryProducts() $this->assertArrayHasKey('products', $response['categories']['items'][0]); $baseCategory = $response['categories']['items'][0]; $this->assertArrayHasKey('total_count', $baseCategory['products']); - //$this->assertGreaterThanOrEqual(1, $baseCategory['products']['total_count']); + $this->assertGreaterThanOrEqual(1, $baseCategory['products']['total_count']); $this->assertEquals(1, $baseCategory['products']['page_info']['current_page']); $this->assertEquals(20, $baseCategory['products']['page_info']['page_size']); $this->assertArrayHasKey('sku', $baseCategory['products']['items'][0]); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 3dcba3277dfa7..03e0aefa3a732 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -186,9 +186,9 @@ public function testQueryChildCategoriesWithProducts() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'], - ['sku' => 'simple', 'name' => 'Simple Product'] + ['sku' => '12345', 'name' => 'Simple Product Two'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children From 34cf76dc80633e2a558aa9f583e8f51604f13417 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Sun, 2 Aug 2020 13:27:36 -0400 Subject: [PATCH 49/65] Updated the default sort to check if the category id is passed as an array. Reverted the tests back to check on products --- .../Product/SearchCriteriaBuilder.php | 15 +++++++-------- .../CategoriesQuery/CategoriesFilterTest.php | 6 +++--- .../Catalog/CategoriesQuery/CategoryTreeTest.php | 2 +- .../Magento/GraphQl/Catalog/CategoryListTest.php | 10 +++++----- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 173d055180119..fe7272b933f75 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -212,14 +212,13 @@ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, ar ->create(); } else { $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; - if ($categoryIdFilter && - !is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) || - count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1 - ) { - $defaultSortOrder[] = $this->sortOrderBuilder - ->setField(EavAttributeInterface::POSITION) - ->setDirection(SortOrder::SORT_ASC) - ->create(); + if ($categoryIdFilter) { + if (!is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) || count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField(EavAttributeInterface::POSITION) + ->setDirection(SortOrder::SORT_ASC) + ->create(); + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 3ba7805ba2d25..679d7659cbf9e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -187,9 +187,9 @@ public function testQueryChildCategoriesWithProducts() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], ['sku' => '12345', 'name' => 'Simple Product Two'], - ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children @@ -297,8 +297,8 @@ public function testQueryCategoryWithDisabledChildren() $this->assertEquals('Its a description of Test Category 1.2', $firstChildCategory['description']); $firstChildCategoryExpectedProducts = [ - ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => 'simple', 'name' => 'Simple Product'] + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] ]; $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; 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 d4089161cd894..c2e82e734cd9b 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 @@ -453,7 +453,7 @@ public function testCategoryProducts() $this->assertAttributes($firstProduct); $this->assertWebsites($firstProductModel, $firstProduct['websites']); $this->assertEquals('Category 1', $firstProduct['categories'][0]['name']); - $this->assertEquals('movable-position-2', $firstProduct['categories'][1]['url_path']); + $this->assertEquals('category-1/category-1-1', $firstProduct['categories'][1]['url_path']); $this->assertCount(3, $firstProduct['categories']); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 03e0aefa3a732..9f2ca5fe6de4d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -214,8 +214,8 @@ public function testQueryChildCategoriesWithProducts() $this->assertEquals('Category 1.2', $secondChildCategory['name']); $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); $firstChildCategoryExpectedProducts = [ - ['sku' => 'simple-4', 'name' => 'Simple Product Three'], ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] ]; $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; @@ -277,9 +277,9 @@ public function testQueryCategoryWithDisabledChildren() $this->assertArrayHasKey('products', $baseCategory); //Check base category products $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'], - ['sku' => 'simple', 'name' => 'Simple Product'] + ['sku' => '12345', 'name' => 'Simple Product Two'] ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children @@ -294,8 +294,8 @@ public function testQueryCategoryWithDisabledChildren() $this->assertEquals('Its a description of Test Category 1.2', $firstChildCategory['description']); $firstChildCategoryExpectedProducts = [ - ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => 'simple', 'name' => 'Simple Product'] + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] ]; $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; From d3aa6b61c467748e17045d8ca0d062a50178dd21 Mon Sep 17 00:00:00 2001 From: dnyomo Date: Sun, 2 Aug 2020 13:45:41 -0400 Subject: [PATCH 50/65] Updated test --- .../Magento/GraphQl/Catalog/CategoryListTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 9f2ca5fe6de4d..43612575a7dcb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -214,8 +214,8 @@ public function testQueryChildCategoriesWithProducts() $this->assertEquals('Category 1.2', $secondChildCategory['name']); $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); $firstChildCategoryExpectedProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], - ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ['sku' => 'simple-4', 'name' => 'Simple Product Three'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; @@ -294,8 +294,8 @@ public function testQueryCategoryWithDisabledChildren() $this->assertEquals('Its a description of Test Category 1.2', $firstChildCategory['description']); $firstChildCategoryExpectedProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], - ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ['sku' => 'simple-4', 'name' => 'Simple Product Three'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; From 7e5d864a2e574e69a571e2e7f4650cd1fb0fff3e Mon Sep 17 00:00:00 2001 From: dnyomo Date: Sun, 2 Aug 2020 15:58:50 -0400 Subject: [PATCH 51/65] missed this test --- .../GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 679d7659cbf9e..a3daf89631c17 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -297,8 +297,8 @@ public function testQueryCategoryWithDisabledChildren() $this->assertEquals('Its a description of Test Category 1.2', $firstChildCategory['description']); $firstChildCategoryExpectedProducts = [ - ['sku' => 'simple', 'name' => 'Simple Product'], - ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ['sku' => 'simple-4', 'name' => 'Simple Product Three'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; From ec098d26e7735528beb8ef338b1eeb25eab3a1a1 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko Date: Mon, 3 Aug 2020 11:01:09 +0100 Subject: [PATCH 52/65] Media modules --- .../Console/Command/Synchronize.php | 70 +++ .../MediaContentSynchronization/LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/Consume.php | 37 ++ .../Model/Publish.php | 45 ++ .../Model/RemoveObsoleteContentAsset.php | 61 +++ .../ResourceModel/GetOutdatedRelations.php | 120 +++++ .../Model/Synchronize.php | 109 ++++ .../Plugin/SynchronizeMediaContent.php | 41 ++ .../MediaContentSynchronization/README.md | 14 + .../MediaContentSynchronization/composer.json | 27 + .../etc/communication.xml | 14 + .../MediaContentSynchronization/etc/di.xml | 21 + .../etc/module.xml | 10 + .../etc/queue_consumer.xml | 11 + .../etc/queue_publisher.xml | 12 + .../etc/queue_topology.xml | 14 + .../registration.php | 14 + .../Api/SynchronizeInterface.php | 21 + .../Api/SynchronizerInterface.php | 21 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/GetEntities.php | 38 ++ .../Model/GetEntitiesInterface.php | 21 + .../Model/SynchronizerPool.php | 51 ++ .../MediaContentSynchronizationApi/README.md | 13 + .../composer.json | 21 + .../MediaContentSynchronizationApi/etc/di.xml | 10 + .../etc/module.xml | 10 + .../registration.php | 14 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/Synchronizer/Category.php | 112 ++++ .../Model/Synchronizer/Product.php | 109 ++++ .../README.md | 13 + .../Model/Synchronizer/CategoryTest.php | 85 ++++ .../Model/Synchronizer/ProductTest.php | 85 ++++ .../composer.json | 24 + .../etc/di.xml | 41 ++ .../etc/module.xml | 10 + .../registration.php | 14 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/Synchronizer/Block.php | 107 ++++ .../Model/Synchronizer/Page.php | 107 ++++ .../MediaContentSynchronizationCms/README.md | 13 + .../Model/Synchronizer/BlockTest.php | 109 ++++ .../Model/Synchronizer/PageTest.php | 109 ++++ .../composer.json | 24 + .../MediaContentSynchronizationCms/etc/di.xml | 39 ++ .../etc/module.xml | 10 + .../registration.php | 14 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../SaveBaseCategoryImageInformation.php | 109 ++++ .../MediaGalleryCatalogIntegration/README.md | 3 + .../composer.json | 28 + .../etc/adminhtml/di.xml | 12 + .../etc/module.xml | 10 + .../registration.php | 14 + .../Controller/Adminhtml/Category/Index.php | 36 ++ .../Magento/MediaGalleryCatalogUi/LICENSE.txt | 48 ++ .../MediaGalleryCatalogUi/LICENSE_AFL.txt | 48 ++ .../Model/Listing/DataProvider.php | 199 ++++++++ .../Magento/MediaGalleryCatalogUi/README.md | 13 + ...sertCategoryGridPageDetailsActionGroup.xml | 20 + .../AdminOpenCategoryGridPageActionGroup.xml | 18 + ...nMediaGalleryCatalogUiCategoryGridPage.xml | 12 + ...diaGalleryCatalogUiCategoryGridSection.xml | 17 + ...lleryCatalogUiUsedInCategoryFilterTest.xml | 63 +++ ...alleryCatalogUiUsedInProductFilterTest.xml | 73 +++ ...eryCatalogUiVerifyCategoryGridPageTest.xml | 31 ++ .../Listing/Columns/CategoryActions.php | 66 +++ .../Ui/Component/Listing/Columns/Path.php | 80 +++ .../Component/Listing/Columns/Thumbnail.php | 91 ++++ .../MediaGalleryCatalogUi/composer.json | 26 + .../etc/adminhtml/di.xml | 41 ++ .../etc/adminhtml/routes.xml | 15 + .../MediaGalleryCatalogUi/etc/module.xml | 10 + .../MediaGalleryCatalogUi/registration.php | 14 + .../media_gallery_catalog_category_index.xml | 15 + .../media_gallery_category_listing.xml | 180 +++++++ .../ui_component/media_gallery_listing.xml | 65 +++ .../standalone_media_gallery_listing.xml | 65 +++ .../adminhtml/web/css/source/_module.less | 23 + .../Controller/Adminhtml/Block/Search.php | 97 ++++ .../Controller/Adminhtml/Page/Search.php | 97 ++++ .../Magento/MediaGalleryCmsUi/LICENSE.txt | 48 ++ .../Magento/MediaGalleryCmsUi/LICENSE_AFL.txt | 48 ++ app/code/Magento/MediaGalleryCmsUi/README.md | 13 + ...FillOutCustomCMSPageContentActionGroup.xml | 30 ++ ...ediaGalleryCmsUiUsedInBlocksFilterTest.xml | 60 +++ ...MediaGalleryCmsUiUsedInPagesFilterTest.xml | 68 +++ .../Magento/MediaGalleryCmsUi/composer.json | 23 + .../MediaGalleryCmsUi/etc/adminhtml/di.xml | 41 ++ .../etc/adminhtml/routes.xml | 15 + .../Magento/MediaGalleryCmsUi/etc/module.xml | 10 + .../MediaGalleryCmsUi/registration.php | 14 + .../ui_component/media_gallery_listing.xml | 62 +++ .../standalone_media_gallery_listing.xml | 62 +++ .../MediaGalleryIntegration/LICENSE.txt | 48 ++ .../MediaGalleryIntegration/LICENSE_AFL.txt | 48 ++ .../Model/OpenDialogUrlProvider.php | 40 ++ .../Plugin/SaveImageInformation.php | 112 ++++ .../Magento/MediaGalleryIntegration/README.md | 3 + .../Model/ImageComponentOpenDialogUrlTest.php | 72 +++ .../Model/OpenDialogUrlProviderTest.php | 63 +++ .../Model/TinyMceOpenDialogUrlTest.php | 78 +++ .../WysiwygDefaultConfigOpenDialogUrlTest.php | 82 +++ .../MediaGalleryIntegration/composer.json | 31 ++ .../etc/adminhtml/di.xml | 17 + .../MediaGalleryIntegration/etc/module.xml | 14 + .../MediaGalleryIntegration/registration.php | 10 + .../Magento/MediaGalleryMetadata/LICENSE.txt | 48 ++ .../MediaGalleryMetadata/LICENSE_AFL.txt | 48 ++ .../Model/AddIptcMetadata.php | 180 +++++++ .../Model/AddXmpMetadata.php | 104 ++++ .../MediaGalleryMetadata/Model/File.php | 79 +++ .../Model/File/AddMetadata.php | 106 ++++ .../Model/File/ExtractMetadata.php | 129 +++++ .../Model/GetIptcMetadata.php | 71 +++ .../Model/GetXmpMetadata.php | 66 +++ .../Model/Gif/ReadFile.php | 318 ++++++++++++ .../Model/Gif/Segment/ReadXmp.php | 97 ++++ .../Model/Gif/Segment/WriteXmp.php | 191 +++++++ .../Model/Gif/WriteFile.php | 76 +++ .../Model/Jpeg/ReadFile.php | 209 ++++++++ .../Model/Jpeg/Segment/ReadIptc.php | 76 +++ .../Model/Jpeg/Segment/ReadXmp.php | 85 ++++ .../Model/Jpeg/Segment/WriteIptc.php | 100 ++++ .../Model/Jpeg/Segment/WriteXmp.php | 156 ++++++ .../Model/Jpeg/WriteFile.php | 92 ++++ .../MediaGalleryMetadata/Model/Metadata.php | 95 ++++ .../Model/Png/ReadFile.php | 127 +++++ .../Model/Png/Segment/ReadIptc.php | 136 +++++ .../Model/Png/Segment/ReadXmp.php | 86 ++++ .../Model/Png/Segment/WriteIptc.php | 214 ++++++++ .../Model/Png/Segment/WriteXmp.php | 163 ++++++ .../Model/Png/WriteFile.php | 78 +++ .../MediaGalleryMetadata/Model/Segment.php | 79 +++ .../Model/SegmentNames.php | 104 ++++ .../Model/XmpTemplate.php | 58 +++ .../Magento/MediaGalleryMetadata/README.md | 3 + .../Integration/Model/AddMetadataTest.php | 197 ++++++++ .../Integration/Model/ExtractMetadataTest.php | 112 ++++ .../Integration/Model/Gif/Segment/XmpTest.php | 117 +++++ .../Model/Jpeg/Segment/IptcTest.php | 134 +++++ .../Model/Jpeg/Segment/XmpTest.php | 117 +++++ .../Model/Png/Segment/IptcTest.php | 134 +++++ .../Test/_files/empty_exiftool.gif | Bin 0 -> 7951 bytes .../Test/_files/empty_iptc.jpeg | Bin 0 -> 19416 bytes .../Test/_files/empty_iptc.png | Bin 0 -> 47596 bytes .../Test/_files/empty_xmp_image.jpeg | Bin 0 -> 19336 bytes .../Test/_files/empty_xmp_image.png | Bin 0 -> 55268 bytes .../Test/_files/exiftool.gif | Bin 0 -> 12704 bytes .../Test/_files/iptc_only.jpeg | Bin 0 -> 19884 bytes .../Test/_files/iptc_only.png | Bin 0 -> 47894 bytes .../Test/_files/macos-photos.jpeg | Bin 0 -> 22795 bytes .../Test/_files/macos-preview.png | Bin 0 -> 57535 bytes .../MediaGalleryMetadata/composer.json | 22 + .../MediaGalleryMetadata/etc/default.xmp | 24 + .../Magento/MediaGalleryMetadata/etc/di.xml | 127 +++++ .../MediaGalleryMetadata/etc/module.xml | 10 + .../MediaGalleryMetadata/registration.php | 14 + .../Api/AddMetadataInterface.php | 27 + .../Api/Data/MetadataInterface.php | 54 ++ .../Api/ExtractMetadataInterface.php | 25 + .../MediaGalleryMetadataApi/LICENSE.txt | 48 ++ .../MediaGalleryMetadataApi/LICENSE_AFL.txt | 48 ++ .../Model/AddMetadataComposite.php | 51 ++ .../Model/ExtractMetadataComposite.php | 78 +++ .../Model/FileInterface.php | 46 ++ .../Model/ReadFileInterface.php | 22 + .../Model/ReadMetadataInterface.php | 28 + .../Model/SegmentInterface.php | 46 ++ .../Model/WriteFileInterface.php | 26 + .../Model/WriteMetadataInterface.php | 24 + .../Magento/MediaGalleryMetadataApi/README.md | 3 + .../MediaGalleryMetadataApi/composer.json | 21 + .../MediaGalleryMetadataApi/etc/module.xml | 10 + .../MediaGalleryMetadataApi/registration.php | 14 + .../MediaGalleryRenditions/LICENSE.txt | 48 ++ .../MediaGalleryRenditions/LICENSE_AFL.txt | 48 ++ .../Magento/MediaGalleryRenditions/README.md | 13 + .../MediaGalleryRenditions/composer.json | 21 + .../etc/adminhtml/system.xml | 24 + .../MediaGalleryRenditions/etc/config.xml | 17 + .../MediaGalleryRenditions/etc/module.xml | 10 + .../MediaGalleryRenditions/registration.php | 14 + .../MediaGalleryRenditionsApi/LICENSE.txt | 48 ++ .../MediaGalleryRenditionsApi/LICENSE_AFL.txt | 48 ++ .../Model/Config.php | 62 +++ .../MediaGalleryRenditionsApi/README.md | 13 + .../MediaGalleryRenditionsApi/composer.json | 21 + .../MediaGalleryRenditionsApi/etc/module.xml | 10 + .../registration.php | 14 + .../Controller/Adminhtml/Asset/Search.php | 169 +++++++ .../Adminhtml/Directories/Create.php | 108 ++++ .../Adminhtml/Directories/Delete.php | 118 +++++ .../Adminhtml/Directories/GetTree.php | 80 +++ .../Controller/Adminhtml/Image/Delete.php | 143 ++++++ .../Controller/Adminhtml/Image/Details.php | 110 ++++ .../Adminhtml/Image/SaveDetails.php | 128 +++++ .../Controller/Adminhtml/Image/Upload.php | 107 ++++ .../Controller/Adminhtml/Index/Index.php | 51 ++ .../Controller/Adminhtml/Media/Index.php | 38 ++ app/code/Magento/MediaGalleryUi/LICENSE.txt | 48 ++ .../Magento/MediaGalleryUi/LICENSE_AFL.txt | 48 ++ .../Model/AssetDetailsProvider/CreatedAt.php | 59 +++ .../Model/AssetDetailsProvider/Height.php | 33 ++ .../Model/AssetDetailsProvider/Size.php | 49 ++ .../Model/AssetDetailsProvider/Type.php | 59 +++ .../Model/AssetDetailsProvider/UpdatedAt.php | 59 +++ .../Model/AssetDetailsProvider/UsedIn.php | 113 +++++ .../Model/AssetDetailsProvider/Width.php | 33 ++ .../Model/AssetDetailsProviderInterface.php | 24 + .../Model/AssetDetailsProviderPool.php | 46 ++ .../Magento/MediaGalleryUi/Model/Config.php | 48 ++ .../MediaGalleryUi/Model/DeleteImage.php | 81 +++ .../Model/Directories/FolderTree.php | 149 ++++++ .../Model/GetDetailsByAssetId.php | 144 ++++++ .../Model/Listing/DataProvider.php | 104 ++++ .../FilterProcessor/ContentField.php | 48 ++ .../FilterProcessor/Directory.php | 26 + .../FilterProcessor/Duplicated.php | 58 +++ .../FilterProcessor/Entity.php | 78 +++ .../FilterProcessor/EntityType.php | 104 ++++ .../FilterProcessor/Keyword.php | 80 +++ .../MediaGalleryUi/Model/UpdateAsset.php | 118 +++++ .../Model/UpdateAsset/SaveMetadataToFile.php | 65 +++ .../Model/UpdateAsset/UpdateKeywords.php | 81 +++ .../MediaGalleryUi/Model/UploadImage.php | 58 +++ .../Plugin/CreateThumbnails.php | 57 +++ app/code/Magento/MediaGalleryUi/README.md | 13 + ...ageInStandaloneMediaGalleryActionGroup.xml | 21 + ...eryAddImageFromImageDetailsActionGroup.xml | 19 + ...alleryApplyDuplicatedFilterActionGroup.xml | 18 + ...cedMediaGalleryApplyFiltersActionGroup.xml | 19 + ...aGalleryAssertActiveFiltersActionGroup.xml | 21 + ...ryAssertImagesDeletedInBulkActionGroup.xml | 18 + ...AssertMassActionModeDetailsActionGroup.xml | 21 + ...sertMassActionModeNotActiveActionGroup.xml | 19 + ...ssertNoActiveFiltersAppliedActionGroup.xml | 17 + ...GalleryAssertWarningMessageActionGroup.xml | 21 + ...eryCategoryGridApplyFiltersActionGroup.xml | 19 + ...eryCategoryGridExpandFilterActionGroup.xml | 19 + ...leryClickDeleteImagesButtonActionGroup.xml | 19 + ...ediaGalleryCloseViewDetailsActionGroup.xml | 19 + ...aGalleryConfirmDeleteImagesActionGroup.xml | 19 + ...dMediaGalleryDeleteGridViewActionGroup.xml | 25 + ...alleryDisableMassactionModeActionGroup.xml | 20 + ...ediaGalleryEditImageDetailsActionGroup.xml | 20 + ...GalleryEnableMassActionModeActionGroup.xml | 19 + ...cedMediaGalleryExpandFilterActionGroup.xml | 19 + ...ncedMediaGalleryImageDeleteActionGroup.xml | 22 + ...iaGalleryImageDetailsDeleteActionGroup.xml | 21 + ...ediaGalleryImageDetailsEditActionGroup.xml | 18 + ...ediaGalleryImageDetailsSaveActionGroup.xml | 24 + ...dMediaGallerySaveCustomViewActionGroup.xml | 24 + ...rySelectCustomBookmarksViewActionGroup.xml | 23 + ...erySelectImageForMassActionActionGroup.xml | 21 + ...iaGallerySelectSourceFilterActionGroup.xml | 22 + ...iaGallerySelectUsedInFilterActionGroup.xml | 25 + ...ncedMediaGalleryUploadImageActionGroup.xml | 24 + ...lleryVerifyImageDescriptionActionGroup.xml | 25 + ...iaGalleryVerifyImageDetailsActionGroup.xml | 37 ++ ...aGalleryVerifyImageFilenameActionGroup.xml | 25 + ...aGalleryVerifyImageKeywordsActionGroup.xml | 25 + ...ediaGalleryVerifyImageTitleActionGroup.xml | 25 + ...ediaGalleryViewImageDetailsActionGroup.xml | 20 + ...diaGalleryApplySelectFilterActionGroup.xml | 23 + ...diaGalleryApplyUsedInFilterActionGroup.xml | 23 + ...tCategoryNameInCategoryGridActionGroup.xml | 21 + ...eryAssertFolderDoesNotExistActionGroup.xml | 18 + ...ediaGalleryAssertFolderNameActionGroup.xml | 17 + ...diaGalleryAssertImageInGridActionGroup.xml | 21 + ...sertImageNotExistsInTheGridActionGroup.xml | 21 + ...ediaGalleryClickAddSelectedActionGroup.xml | 16 + ...ediaGalleryClickImageInGridActionGroup.xml | 21 + ...alleryClickOkButtonTinyMce4ActionGroup.xml | 20 + ...MediaGalleryCreateNewFolderActionGroup.xml | 18 + ...aGalleryEditAssetAddKeywordActionGroup.xml | 22 + ...nMediaGalleryEnhancedEnableActionGroup.xml | 24 + ...minMediaGalleryFolderDeleteActionGroup.xml | 17 + ...minMediaGalleryFolderSelectActionGroup.xml | 19 + ...dminMediaGalleryImageDeleteActionGroup.xml | 21 + ...diaGalleryOpenNewFolderFormActionGroup.xml | 15 + ...ryFromCategoryImageUploaderActionGroup.xml | 20 + ...ediaGalleryFromPageNoEditorActionGroup.xml | 20 + ...ediaGalleryFromTinyMce4IconActionGroup.xml | 22 + ...nOpenStandaloneMediaGalleryActionGroup.xml | 15 + ...cedMediaGalleryImageDeletedActionGroup.xml | 20 + ...sertImageAddedToPageContentActionGroup.xml | 24 + ...butesOnEnhancedMediaGalleryActionGroup.xml | 36 ++ ...lleryAdminDataGridByKeywordActionGroup.xml | 19 + ...tImageInCategoryDescriptionActionGroup.xml | 26 + .../AdminEnhancedMediaGalleryImageData.xml | 43 ++ .../Mftf/Data/AdminMediaGalleryFolderData.xml | 17 + .../Test/Mftf/Data/AdobeStockConfigData.xml | 18 + .../Page/AdminStandaloneMediaGalleryPage.xml | 12 + .../Mftf/Section/AdminConfigSystemSection.xml | 15 + ...dminEnhancedMediaGalleryActionsSection.xml | 17 + ...EnhancedMediaGalleryDeleteModalSection.xml | 14 + ...EnhancedMediaGalleryEditDetailsSection.xml | 20 + ...dminEnhancedMediaGalleryFiltersSection.xml | 29 ++ ...nhancedMediaGalleryImageActionsSection.xml | 17 + ...cedMediaGalleryImageDescriptionSection.xml | 15 + ...nEnhancedMediaGalleryMassActionSection.xml | 16 + ...EnhancedMediaGalleryViewDetailsSection.xml | 24 + .../AdminMediaGalleryFolderSection.xml | 22 + .../AdminMediaGalleryHeaderButtonsSection.xml | 15 + .../Test/Mftf/Suite/MediaGalleryUiSuite.xml | 29 ++ ...ncedMediaGalleryDeleteImagesInBulkTest.xml | 50 ++ ...hancedMediaGalleryDuplicatedImagesTest.xml | 55 ++ ...ediaGalleryUploadImageWithMetadataTest.xml | 70 +++ ...ancedMediaGalleryVerifyAssetFilterTest.xml | 80 +++ ...iaGalleryVerifyNotUsedOptionFilterTest.xml | 79 +++ ...ncedMediaGalleryVerifyUsedInFilterTest.xml | 83 +++ ...yAddCategoryImageFromTwoComponentsTest.xml | 78 +++ .../AdminMediaGalleryAddCategoryImageTest.xml | 55 ++ ...minMediaGalleryAddFromImageDetailsTest.xml | 41 ++ ...dminMediaGalleryCreateDeleteFolderTest.xml | 57 +++ ...MediaGalleryDeleteImageContextMenuTest.xml | 33 ++ .../AdminMediaGalleryDeleteImageFileTest.xml | 41 ++ ...GalleryDeleteImageWithWarningPopupTest.xml | 56 ++ ...nMediaGalleryDisabledContentFilterTest.xml | 65 +++ .../AdminMediaGalleryEditImageDetailsTest.xml | 48 ++ ...inMediaGalleryEnabledContentFilterTest.xml | 57 +++ ...inMediaGalleryFilterImagesBySourceTest.xml | 53 ++ .../AdminMediaGallerySaveFiltersStateTest.xml | 45 ++ ...ediaGalleryStoreViewCategoryFilterTest.xml | 62 +++ ...MediaGalleryStoreViewContentFilterTest.xml | 61 +++ ...minMediaGalleryUploadCategoryImageTest.xml | 43 ++ ...iaGalleryVerifyImageGridAttributesTest.xml | 37 ++ ...MediaGalleryViewDetailsDeleteImageTest.xml | 37 ++ .../AdminMediaGalleryViewDetailsEditTest.xml | 46 ++ .../Test/AdminMediaGalleryViewDetailsTest.xml | 40 ++ ...loneMediaGalleryCreateDeleteFolderTest.xml | 56 ++ ...daloneMediaGalleryEditImageDetailsTest.xml | 47 ++ ...ndaloneMediaGalleryViewDetailsEditTest.xml | 55 ++ ...nStandaloneMediaGalleryViewDetailsTest.xml | 40 ++ .../Test/Unit/Model/ConfigTest.php | 64 +++ .../Test/Unit/Model/UploadImageTest.php | 136 +++++ .../Test/Unit/_files/subdir/test_img2.jpeg | Bin 0 -> 58077 bytes .../Test/Unit/_files/test_img1.jpeg | Bin 0 -> 58077 bytes .../Ui/Component/DirectoriesTree.php | 60 +++ .../Ui/Component/ImageUploader.php | 71 +++ .../Ui/Component/ImageUploaderStandAlone.php | 36 ++ .../Listing/Columns/SourceIconProvider.php | 106 ++++ .../Ui/Component/Listing/Columns/Url.php | 119 +++++ .../Ui/Component/Listing/Filters/Asset.php | 102 ++++ .../Listing/Filters/Options/Status.php | 27 + .../Listing/Filters/Options/Store.php | 42 ++ .../Listing/Filters/Options/UsedIn.php | 37 ++ .../Ui/Component/Listing/Provider.php | 88 ++++ app/code/Magento/MediaGalleryUi/composer.json | 30 ++ .../MediaGalleryUi/etc/adminhtml/di.xml | 70 +++ .../MediaGalleryUi/etc/adminhtml/menu.xml | 13 + .../MediaGalleryUi/etc/adminhtml/routes.xml | 15 + .../MediaGalleryUi/etc/adminhtml/system.xml | 21 + .../Magento/MediaGalleryUi/etc/config.xml | 16 + app/code/Magento/MediaGalleryUi/etc/di.xml | 59 +++ .../Magento/MediaGalleryUi/etc/module.xml | 14 + .../Magento/MediaGalleryUi/i18n/en_US.csv | 8 + .../Magento/MediaGalleryUi/registration.php | 10 + .../layout/media_gallery_index_index.xml | 32 ++ .../layout/media_gallery_media_index.xml | 26 + .../view/adminhtml/templates/container.phtml | 29 ++ .../adminhtml/templates/image_details.phtml | 109 ++++ .../templates/image_details_standalone.phtml | 101 ++++ .../templates/image_edit_details.phtml | 97 ++++ .../image_edit_details_standalone.phtml | 99 ++++ .../ui_component/cms_block_listing.xml | 38 ++ .../ui_component/cms_page_listing.xml | 38 ++ .../ui_component/media_gallery_listing.xml | 393 ++++++++++++++ .../ui_component/product_listing.xml | 38 ++ .../standalone_media_gallery_listing.xml | 380 ++++++++++++++ .../adminhtml/web/css/source/_module.less | 478 ++++++++++++++++++ .../view/adminhtml/web/images/3-dots.png | Bin 0 -> 3533 bytes .../view/adminhtml/web/images/Astock.png | Bin 0 -> 359 bytes .../view/adminhtml/web/images/d.png | Bin 0 -> 12159 bytes .../deleteImageWithDetailConfirmation.js | 75 +++ .../adminhtml/web/js/action/deleteImages.js | 130 +++++ .../adminhtml/web/js/action/getDetails.js | 60 +++ .../adminhtml/web/js/action/saveDetails.js | 56 ++ .../view/adminhtml/web/js/container.js | 34 ++ .../js/directory/actions/createDirectory.js | 61 +++ .../js/directory/actions/deleteDirectory.js | 60 +++ .../adminhtml/web/js/directory/directories.js | 186 +++++++ .../web/js/directory/directoryTree.js | 477 +++++++++++++++++ .../adminhtml/web/js/grid/columns/image.js | 288 +++++++++++ .../web/js/grid/columns/image/actions.js | 109 ++++ .../grid/columns/image/insertImageAction.js | 131 +++++ .../view/adminhtml/web/js/grid/masonry.js | 49 ++ .../web/js/grid/massaction/massactionView.js | 147 ++++++ .../web/js/grid/massaction/massactions.js | 151 ++++++ .../view/adminhtml/web/js/grid/messages.js | 77 +++ .../view/adminhtml/web/js/grid/sortBy.js | 77 +++ .../view/adminhtml/web/js/image-uploader.js | 245 +++++++++ .../adminhtml/web/js/image/image-actions.js | 130 +++++ .../adminhtml/web/js/image/image-details.js | 174 +++++++ .../view/adminhtml/web/js/image/image-edit.js | 228 +++++++++ .../validation/validate-image-description.js | 19 + .../js/validation/validate-image-keyword.js | 19 + .../web/js/validation/validate-image-title.js | 19 + .../web/template/grid/columns/image.html | 45 ++ .../template/grid/columns/image/actions.html | 15 + .../grid/directories/directoryTree.html | 10 + .../web/template/grid/filter/checkbox.html | 24 + .../grid/filters/elements/ui-select.html | 133 +++++ .../grid/massactions/cancelButton.html | 10 + .../web/template/grid/massactions/count.html | 9 + .../adminhtml/web/template/grid/messages.html | 15 + .../adminhtml/web/template/grid/toolbar.html | 32 ++ .../web/template/image-uploader.html | 17 + .../adminhtml/web/template/image/actions.html | 12 + .../web/template/image/image-details.html | 65 +++ .../web/template/image/image-edit.html | 74 +++ .../MediaGalleryUiApi/Api/ConfigInterface.php | 22 + .../Magento/MediaGalleryUiApi/LICENSE.txt | 48 ++ .../Magento/MediaGalleryUiApi/LICENSE_AFL.txt | 48 ++ app/code/Magento/MediaGalleryUiApi/README.md | 13 + .../Magento/MediaGalleryUiApi/composer.json | 21 + .../Magento/MediaGalleryUiApi/etc/module.xml | 10 + .../MediaGalleryUiApi/registration.php | 10 + composer.json | 14 + composer.lock | 2 +- 427 files changed, 23252 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php create mode 100644 app/code/Magento/MediaContentSynchronization/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronization/Model/Consume.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/Publish.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/Synchronize.php create mode 100644 app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php create mode 100644 app/code/Magento/MediaContentSynchronization/README.md create mode 100644 app/code/Magento/MediaContentSynchronization/composer.json create mode 100644 app/code/Magento/MediaContentSynchronization/etc/communication.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml create mode 100644 app/code/Magento/MediaContentSynchronization/registration.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizerInterface.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/README.md create mode 100644 app/code/Magento/MediaContentSynchronizationApi/composer.json create mode 100644 app/code/Magento/MediaContentSynchronizationApi/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronizationApi/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronizationApi/registration.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/README.md create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/composer.json create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/registration.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/README.md create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/composer.json create mode 100644 app/code/Magento/MediaContentSynchronizationCms/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCms/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCms/registration.php create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/README.md create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/composer.json create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/registration.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/README.md create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/composer.json create mode 100644 app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/registration.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less create mode 100644 app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryCmsUi/README.md create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/composer.json create mode 100644 app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/registration.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryIntegration/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php create mode 100644 app/code/Magento/MediaGalleryIntegration/README.md create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/composer.json create mode 100644 app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryIntegration/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryIntegration/registration.php create mode 100644 app/code/Magento/MediaGalleryMetadata/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/File.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Metadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Segment.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php create mode 100644 app/code/Magento/MediaGalleryMetadata/README.md create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png create mode 100644 app/code/Magento/MediaGalleryMetadata/composer.json create mode 100644 app/code/Magento/MediaGalleryMetadata/etc/default.xmp create mode 100644 app/code/Magento/MediaGalleryMetadata/etc/di.xml create mode 100644 app/code/Magento/MediaGalleryMetadata/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryMetadata/registration.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/README.md create mode 100644 app/code/Magento/MediaGalleryMetadataApi/composer.json create mode 100644 app/code/Magento/MediaGalleryMetadataApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryMetadataApi/registration.php create mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryRenditions/README.md create mode 100644 app/code/Magento/MediaGalleryRenditions/composer.json create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/config.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/registration.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/README.md create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/composer.json create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/registration.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php create mode 100644 app/code/Magento/MediaGalleryUi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/Config.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/DeleteImage.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UploadImage.php create mode 100644 app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php create mode 100644 app/code/Magento/MediaGalleryUi/README.md create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php create mode 100644 app/code/Magento/MediaGalleryUi/composer.json create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/config.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/di.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryUi/i18n/en_US.csv create mode 100644 app/code/Magento/MediaGalleryUi/registration.php create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html create mode 100644 app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php create mode 100644 app/code/Magento/MediaGalleryUiApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryUiApi/README.md create mode 100644 app/code/Magento/MediaGalleryUiApi/composer.json create mode 100644 app/code/Magento/MediaGalleryUiApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryUiApi/registration.php diff --git a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..55f99697c289b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php @@ -0,0 +1,70 @@ +synchronizeContent = $synchronizeContent; + $this->state = $state; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-content:sync'); + $this->setDescription('Synchronize content with assets'); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing content with assets...'); + $this->state->emulateAreaCode( + Area::AREA_ADMINHTML, + function () { + $this->synchronizeContent->execute(); + } + ); + $output->writeln('Completed content synchronization.'); + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/LICENSE.txt b/app/code/Magento/MediaContentSynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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/MediaContentSynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php new file mode 100644 index 0000000000000..bcce3514e4ad9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -0,0 +1,37 @@ +synchronize = $synchronize; + } + + /** + * Run media files synchronization. + */ + public function execute() : void + { + $this->synchronize->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php new file mode 100644 index 0000000000000..ad6fdd27d7067 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -0,0 +1,45 @@ +publisher = $publisher; + } + + /** + * Publish media content synchronization message to the message queue. + */ + public function execute() : void + { + $this->publisher->publish( + self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, + [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION] + ); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php new file mode 100644 index 0000000000000..e81817282dcc0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php @@ -0,0 +1,61 @@ +deleteContentAssetLinks = $deleteContentAssetLinks; + $this->getEntities = $getEntities; + $this->getOutdatedRelations = $getOutdatedRelations; + } + + /** + * Remove media content if entity already deleted. + */ + public function execute(): void + { + foreach ($this->getEntities->execute() as $entity) { + $assetsLinks = $this->getOutdatedRelations->execute($entity); + if (!empty($assetsLinks)) { + $this->deleteContentAssetLinks->execute($assetsLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php new file mode 100644 index 0000000000000..37271ce469715 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php @@ -0,0 +1,120 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Returns content asset links wichs entity_id not exist anymore. + * + * @param string $entityType + * @throws CouldNotDeleteException + * @return ContentAssetLinkInterface[] + */ + public function execute(string $entityType): array + { + $contentAssetLinks= []; + try { + $entityData = $this->metadataPool->getMetadata($entityType); + $connection = $this->resourceConnection->getConnection(); + $mediaContentTable = $this->resourceConnection->getTableName(self::MEDIA_CONTENT_ASSET_TABLE); + $select = $connection->select(); + + $select->from(['mca' => $mediaContentTable], ['asset_id', 'entity_id', 'entity_type', 'field']); + $select->joinLeft( + ['et' => $entityData->getEntityTable()], + 'et.' . $entityData->getIdentifierField() . ' = mca.entity_id ', + [$entityData->getIdentifierField() . ' AS entity_identifier'] + ); + $select->where('et.' . $entityData->getIdentifierField() . ' IS NULL'); + $select->where('mca.entity_type = ?', $entityData->getEavEntityType() ?? $entityData->getEntityTable()); + $assets = $connection->fetchAll($select); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException(__('Could not fetch media content links data'), $exception); + } + + foreach ($assets as $asset) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => $asset['entity_type'], + 'entityId' => $asset['entity_id'], + 'field' => $asset['field'] + ] + ); + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset['asset_id'], + 'contentIdentity' => $contentIdentity + ] + ); + } + + return $contentAssetLinks; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..cea8cc6ad44da --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php @@ -0,0 +1,109 @@ +removeObsoleteContent = $removeObsoleteContent; + $this->dateFactory = $dateFactory; + $this->flagManager = $flagManager; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + try { + $synchronizer->execute(); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following content synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + + $this->setLastExecutionTime(); + $this->removeObsoleteContent->execute(); + } + + /** + * Set last synchronizer execution time + */ + private function setLastExecutionTime(): void + { + $currentTime = $this->dateFactory->create()->gmtDate(); + $this->flagManager->saveFlag(self::LAST_EXECUTION_TIME_CODE, $currentTime); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php new file mode 100644 index 0000000000000..e428f7d273bb4 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php @@ -0,0 +1,41 @@ +publish = $publish; + } + + /** + * Publish content synchronization request message to the queue. + * + * @param Consume $subject + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(Consume $subject): void + { + $this->publish->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/README.md b/app/code/Magento/MediaContentSynchronization/README.md new file mode 100644 index 0000000000000..69098ab02eb0b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaContentSynchronization module + +The Magento_MediaContentSynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronization module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json new file mode 100644 index 0000000000000..3be5f535487ec --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-media-content-synchronization", + "description": "Magento module provides implementation of the media content data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/framework-message-queue": "*", + "magento/module-media-content-api": "*" + }, + "suggest": { + "magento/module-media-gallery-synchronization": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml new file mode 100644 index 0000000000000..e3436aee85331 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml new file mode 100644 index 0000000000000..d4615c15206e5 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -0,0 +1,21 @@ + + + + + + + + Magento\MediaContentSynchronization\Console\Command\Synchronize + + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/module.xml b/app/code/Magento/MediaContentSynchronization/etc/module.xml new file mode 100644 index 0000000000000..7f04d9b57d8a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..6a141c04c59a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9751d1161b2f2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..4dc43ef1ac13f --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/registration.php b/app/code/Magento/MediaContentSynchronization/registration.php new file mode 100644 index 0000000000000..a157f7ec90a6a --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/registration.php @@ -0,0 +1,14 @@ +" 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/MediaContentSynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/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/MediaContentSynchronizationApi/Model/GetEntities.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php new file mode 100644 index 0000000000000..38129b2b1c6b9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php @@ -0,0 +1,38 @@ +entities = $entities; + } + + /** + * Get all entities configuration used in media content. + * + * @return array + */ + public function execute(): array + { + return $this->entities; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php new file mode 100644 index 0000000000000..ad62ae4136378 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php @@ -0,0 +1,21 @@ +synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizerInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/README.md b/app/code/Magento/MediaContentSynchronizationApi/README.md new file mode 100644 index 0000000000000..25ceae24452f1 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentSynchronizationApi module + +The Magento_MediaContentSynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronizationApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json new file mode 100644 index 0000000000000..1f1e5e4b51c5b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-content-synchronization-api", + "description": "Magento module responsible for the media content synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..76bdd9b1cb162 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..3a149b31da3cb --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationApi/registration.php b/app/code/Magento/MediaContentSynchronizationApi/registration.php new file mode 100644 index 0000000000000..965e31fa45516 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/registration.php @@ -0,0 +1,14 @@ +" 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/MediaContentSynchronizationCatalog/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/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/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php new file mode 100644 index 0000000000000..665a22b045e44 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php @@ -0,0 +1,112 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [ + self::CATEGORY_IDENTITY_FIELD, + self::CATEGORY_UPDATED_AT_FIELD + ]; + foreach ($this->fetchBatches->execute(self::CATEGORY_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CATEGORY_IDENTITY_FIELD] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php new file mode 100644 index 0000000000000..5d72399752602 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php @@ -0,0 +1,109 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fetchBatches = $fetchBatches; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [self::PRODUCT_TABLE_ENTITY_ID, self::PRODUCT_TABLE_UPDATED_AT_FIELD]; + foreach ($this->fetchBatches->execute(self::PRODUCT_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::PRODUCT_TABLE_ENTITY_ID] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/README.md b/app/code/Magento/MediaContentSynchronizationCatalog/README.md new file mode 100644 index 0000000000000..8395ffc10d4d2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCatalog module + +The Magento_MediaContentCatalog provides the implementation of MediaContentSyncronization functionality for Magento_Catalog module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php new file mode 100644 index 0000000000000..b8f12bad6bd77 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php @@ -0,0 +1,85 @@ +synchronizer = Bootstrap::getObjectManager()->get(Category::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between category and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $categoryId = 28767; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => $categoryId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($categoryId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php new file mode 100644 index 0000000000000..247fdf4a770ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php @@ -0,0 +1,85 @@ +synchronizer = Bootstrap::getObjectManager()->get(Product::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between products and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $productId = 1567; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => $productId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($productId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json new file mode 100644 index 0000000000000..733f29d3a42c2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-catalog", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Catalog module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCatalog\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml new file mode 100644 index 0000000000000..8cc86fde8fbcd --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -0,0 +1,41 @@ + + + + + + + image + description + + + + + + + Magento\Catalog\Api\Data\ProductInterface + Magento\Catalog\Api\Data\CategoryInterface + + + + + + + description + short_description + + + + + + + Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Category + Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml new file mode 100644 index 0000000000000..9660dcb107b45 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/registration.php b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php new file mode 100644 index 0000000000000..1e8b47dc15b50 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php @@ -0,0 +1,14 @@ +" 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/MediaContentSynchronizationCms/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/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/MediaContentSynchronizationCms/Model/Synchronizer/Block.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php new file mode 100644 index 0000000000000..73586c8daf7f3 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php @@ -0,0 +1,107 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * Synchronize assets and contents + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_BLOCK_TABLE_ENTITY_ID, + self::CMS_BLOCK_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_BLOCK_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize block entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_BLOCK_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php new file mode 100644 index 0000000000000..dcc855940d157 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php @@ -0,0 +1,107 @@ +fetchBatches = $fetchBatches; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_PAGE_TABLE_ENTITY_ID, + self::CMS_PAGE_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_PAGE_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize page entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_PAGE_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/README.md b/app/code/Magento/MediaContentSynchronizationCms/README.md new file mode 100644 index 0000000000000..58582b1b2d706 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCms module + +The Magento_MediaContentCms provides the implementation of MediaContentSyncronization functionality for Magento_Cms module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php new file mode 100644 index 0000000000000..2737ab524584b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php @@ -0,0 +1,109 @@ +synchronizer = Bootstrap::getObjectManager()->get(Block::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between blocks and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($blockId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * Get fixture block + * + * @param string $identifier + * @return BlockInterface + * @throws LocalizedException + */ + private function getBlock(string $identifier): BlockInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = $objectManager->get(BlockRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier) + ->create(); + + return current($blockRepository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php new file mode 100644 index 0000000000000..1dcbb96dc7914 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php @@ -0,0 +1,109 @@ +synchronizer = Bootstrap::getObjectManager()->get(Page::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between pages and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($pageId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * Get fixture page + * + * @param string $identifier + * @return PageInterface + * @throws LocalizedException + */ + private function getPage(string $identifier): PageInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var PageRepositoryInterface $repository */ + $repository = $objectManager->get(PageRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier) + ->create(); + + return current($repository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/composer.json b/app/code/Magento/MediaContentSynchronizationCms/composer.json new file mode 100644 index 0000000000000..9028b9dacd0a2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-cms", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Cms module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCms\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml new file mode 100644 index 0000000000000..7def330298789 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -0,0 +1,39 @@ + + + + + + + Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block + Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page + + + + + + + Magento\Cms\Api\Data\BlockInterface + Magento\Cms\Api\Data\PageInterface + + + + + + + content + + + + + + + content + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml new file mode 100644 index 0000000000000..58497b81a2174 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCms/registration.php b/app/code/Magento/MediaContentSynchronizationCms/registration.php new file mode 100644 index 0000000000000..13ed4b73f70ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/registration.php @@ -0,0 +1,14 @@ +" 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/MediaGalleryCatalogIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/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/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php new file mode 100644 index 0000000000000..d439b53c120cb --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -0,0 +1,109 @@ +deleteAssetsByPaths = $deleteAssetsByPath; + $this->filesystem = $filesystem; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->storage = $storage; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + } + + /** + * Saves base category image information after moving from tmp folder. + * + * @param ImageUploader $subject + * @param string $imagePath + * @return string + * @throws LocalizedException + */ + public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string + { + if (!$this->config->isEnabled()) { + return $imagePath; + } + + $absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath; + $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1); + $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); + + if (!empty($tmpAssets)) { + $this->deleteAssetsByPaths->execute([$tmpAssets[0]->getPath()]); + } + + $this->synchronizeFiles->execute( + [ + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getRelativePath($absolutePath) + ] + ); + + return $imagePath; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/README.md b/app/code/Magento/MediaGalleryCatalogIntegration/README.md new file mode 100644 index 0000000000000..bcb37bd486dab --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryCatalogIntegration + +The purpose of this module is for extending catalog image uploader functionality. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json new file mode 100644 index 0000000000000..efabb70da9f39 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-gallery-catalog-integration", + "description": "Magento module responsible for extending catalog image uploader functionality", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-gallery-ui-api": "*" + }, + "suggest": { + "magento/module-catalog": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..2f8fab34911d6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml new file mode 100644 index 0000000000000..c9f1164121e91 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/registration.php b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php new file mode 100644 index 0000000000000..9495790092df1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php @@ -0,0 +1,14 @@ +resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->getConfig()->getTitle()->prepend(__('Categories')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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. diff --git a/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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/MediaGalleryCatalogUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..e17b02ec40737 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php @@ -0,0 +1,199 @@ +categoryList = $categoryList; + $this->searchResultFactory = $searchResultFactory; + $this->attributeValueFactory = $attributeValueFactory; + $this->documentFactory = $documentFactory; + $this->filterGroupBuilder = $filterGroupBuilder; + } + + /** + * @inheritdoc + */ + public function getData() + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + $searchCriteria = $this->getSearchCriteria(); + $searchCriteria = $this->skipRootCategory($searchCriteria); + $collection = $this->categoryList->getList($searchCriteria); + $items = []; + + foreach ($collection->getItems() as $category) { + $items[] = $this->createDocument( + [ + 'entity_id' => $category->getEntityId(), + 'name' => $category->getName(), + 'image' => $category->getImage(), + 'path' => $category->getPath(), + 'display_mode' => $category->getDisplayMode(), + 'products' => $category->getProductCount(), + 'include_in_menu' => $category->getIncludeInMenu(), + 'is_active' => $category->getIsActive() + ] + ); + } + + $searchResult = $this->searchResultFactory->create(); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($items); + $searchResult->setTotalCount($collection->getTotalCount()); + + return $searchResult; + } + + /** + * Skip empty root category in collection + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchCriteriaInterface + */ + private function skipRootCategory(SearchCriteriaInterface $searchCriteria): SearchCriteriaInterface + { + $filterGroups = $searchCriteria->getFilterGroups(); + + $filters[] = $this->filterBuilder + ->setField(self::ENTITY_ID) + ->setConditionType('neq') + ->setValue(1) + ->create(); + $filterGroups[] = $this->filterGroupBuilder->setFilters($filters)->create(); + $searchCriteria->setFilterGroups($filterGroups); + return $searchCriteria; + } + + /** + * Add attributes to grid result + * + * @param array $attributes [code => value] + */ + private function createDocument(array $attributes): Document + { + $item = $this->documentFactory->create(); + $customAttributes = []; + + foreach ($attributes as $code => $value) { + $attribute = $this->attributeValueFactory->create(); + $attribute->setAttributeCode($code); + $attribute->setValue($value); + $customAttributes[$code] = $attribute; + } + + $item->setCustomAttributes($customAttributes); + + return $item; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md new file mode 100644 index 0000000000000..f47b031875f5d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCatalogUi module + +The Magento_MediaGalleryCatalogUi module that implement category grid for media gallery. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml new file mode 100644 index 0000000000000..0788bbd60291a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + Assert category grid page basic columns values for default category + + + + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml new file mode 100644 index 0000000000000..2444cb314ad22 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + Navigates to category grid page by link. + + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml new file mode 100644 index 0000000000000..99cee48f443c7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml @@ -0,0 +1,12 @@ + + + + +
+ + diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml new file mode 100644 index 0000000000000..1f1ce05222e7e --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + +
+
diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml new file mode 100644 index 0000000000000..a495e2ff07e6a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + <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/4951846"/> + <description value="User filters assets used in categories"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </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="clickAddSelectedContentImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderAgain"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Categories"/> + <argument name="optionName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml new file mode 100644 index 0000000000000..d68fd4cb7cca8 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -0,0 +1,73 @@ +<?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="AdminMediaGalleryCatalogUiUsedInProductFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInProductsFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in products filter"/> + <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/4951848"/> + <description value="User filters assets used in products"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab"/> + <waitForElementVisible selector="{{CatalogWYSIWYGSection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{CatalogWYSIWYGSection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Products"/> + <argument name="optionName" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml new file mode 100644 index 0000000000000..6b7bd3ba11f45 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml @@ -0,0 +1,31 @@ +<?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="AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User sees category entities where asset is used in"/> + <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 sees category entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminAssertCategoryGridPageDetailsActionGroup" stepKey="assertCategoryGridPageRendered"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php new file mode 100644 index 0000000000000..0e7edd53bb45d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\UrlInterface; + +/** + * Class CategoryActions for Category grid + */ +class CategoryActions extends Column +{ + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param UrlInterface $urlBuilder + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + UrlInterface $urlBuilder, + array $components = [], + array $data = [] + ) { + $this->urlBuilder = $urlBuilder; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')]['edit'] = [ + 'href' => $this->urlBuilder->getUrl( + 'catalog/category/edit', + [ + 'id' => $item['entity_id'] + ] + ), + 'label' => __('Edit'), + 'hidden' => false, + ]; + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php new file mode 100644 index 0000000000000..38569f5f698da --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Path column for Category grid + */ +class Path extends Column +{ + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param CategoryRepositoryInterface $categoryRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + CategoryRepositoryInterface $categoryRepository, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->categoryRepository = $categoryRepository; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName] = $this->getCategoryPathWithNames($item[$fieldName]); + } + } + } + + return $dataSource; + } + + /** + * Replace category path ids with category names + * + * @param string $pathWithIds + */ + private function getCategoryPathWithNames(string $pathWithIds): string + { + $categoryPathWithName = ''; + $categoryIds = explode('/', $pathWithIds); + foreach ($categoryIds as $id) { + if ($id == 1) { + continue; + } + $categoryName = $this->categoryRepository->get($id)->getName(); + $categoryPathWithName .= ' / ' . $categoryName; + } + return $categoryPathWithName; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php new file mode 100644 index 0000000000000..efb2ad2f8dae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Helper\Image; +use Magento\Framework\DataObject; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Thumbnail column for Category grid + */ +class Thumbnail extends Column +{ + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Image + */ + private $imageHelper; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param Image $image + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + Image $image, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->imageHelper = $image; + $this->storeManager = $storeManager; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); + } else { + $category = new DataObject($item); + $imageHelper = $this->imageHelper->init($category, 'product_listing_thumbnail'); + $item[$fieldName . '_src'] = $imageHelper->getUrl(); + } + } + } + + return $dataSource; + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl() . $path; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json new file mode 100644 index 0000000000000..985d581beff25 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-media-gallery-catalog-ui", + "description": "Magento module that implement category grid for media gallery.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-store": "*", + "magento/module-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..500ac10f4745a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?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"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="product_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product</item> + <item name="category_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_product</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_category</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="catalog_category" xsi:type="array"> + <item name="name" xsi:type="string">Categories</item> + <item name="link" xsi:type="string">media_gallery_catalog/category/index</item> + </item> + <item name="catalog_product" xsi:type="array"> + <item name="name" xsi:type="string">Products</item> + <item name="link" xsi:type="string">catalog/product/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..45f1ccce1c64f --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?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:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_catalog" frontName="media_gallery_catalog"> + <module name="Magento_MediaGalleryCatalogUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml new file mode 100644 index 0000000000000..4a593cbf10901 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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_MediaGalleryCatalogUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/registration.php b/app/code/Magento/MediaGalleryCatalogUi/registration.php new file mode 100644 index 0000000000000..c0376e2a828d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCatalogUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml new file mode 100644 index 0000000000000..1e195efc1beab --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-category-container" name="content"> + <uiComponent name="media_gallery_category_listing"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml new file mode 100644 index 0000000000000..e0b9eacbb4d20 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_category_listing.media_gallery_category_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_category_columns</spinner> + <deps> + <dep>media_gallery_category_listing.media_gallery_category_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_category_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">entity_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryCatalogUi\Model\Listing\DataProvider" name="media_gallery_category_listing_data_source"> + <settings> + <requestFieldName>entity_id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="name" > + <settings> + <placeholder>Search by category name</placeholder> + <label>Name</label> + </settings> + </filterSearch> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">catalog_category</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + </listingToolbar> + <columns name="media_gallery_category_columns"> + <column name="entity_id"> + <settings> + <filter>text</filter> + <label translate="true">ID</label> + </settings> + </column> + <column name="image" component="Magento_Ui/js/grid/columns/thumbnail" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Thumbnail"> + <settings> + <sortable>false</sortable> + <label translate="true">Image</label> + </settings> + </column> + <column name="name"> + <settings> + <label translate="true">Name</label> + </settings> + </column> + <column name="path" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Path"> + <settings> + <label translate="true">Path</label> + </settings> + </column> + <column name="display_mode"> + <settings> + <filter>text</filter> + <label translate="true">Display Mode</label> + </settings> + </column> + <column name="products"> + <settings> + <label translate="true">Products</label> + </settings> + </column> + <column name="include_in_menu" component="Magento_Ui/js/grid/columns/select"> + <argument name="data" xsi:type="array"> + </argument> + <settings> + <options> + <option name="Yes" xsi:type="array"> + <item name="value" xsi:type="number">1</item> + <item name="label" xsi:type="string">Yes</item> + </option> + <option name="No" xsi:type="array"> + <item name="value" xsi:type="number">0</item> + <item name="label" xsi:type="string">No</item> + </option> + </options> + <dataType>select</dataType> + <filter>select</filter> + <label translate="true">In Menu</label> + </settings> + </column> + <column name="is_active" component="Magento_Ui/js/grid/columns/select" > + <settings> + <dataType>select</dataType> + <filter>select</filter> + <options> + <option name="Yes" xsi:type="array"> + <item name="value" xsi:type="number">1</item> + <item name="label" xsi:type="string">Yes</item> + </option> + <option name="No" xsi:type="array"> + <item name="value" xsi:type="number">0</item> + <item name="label" xsi:type="string">No</item> + </option> + </options> + <label translate="true">Enabled</label> + </settings> + </column> + <actionsColumn name="actions" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\CategoryActions" sortOrder="1000"> + <settings> + <indexField>entity_id</indexField> + </settings> + </actionsColumn> + </columns> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..97743b458e8d7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + component="Magento_Catalog/js/components/product-ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> + <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..97743b458e8d7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + component="Magento_Catalog/js/components/product-ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> + <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..0d2a1897e0c25 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,23 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +& when (@media-common = true) { + + .media-gallery-category-container { + + .admin__field-label { + text-align: left; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php new file mode 100644 index 0000000000000..7beb95375073e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Block; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search blocks for ui-select component + */ +class Search extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::block'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param BlockRepositoryInterface $blockRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + BlockRepositoryInterface $blockRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->blockRepository = $blockRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute() : ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->blockRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $block) { + $id = $block->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php new file mode 100644 index 0000000000000..b211e58a0e8c6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Page; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search pages for ui-select component + */ +class Search extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::page'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param PageRepositoryInterface $pageRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + PageRepositoryInterface $pageRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->pageRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $page) { + $id = $page->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md new file mode 100644 index 0000000000000..a5c2eb24c6c15 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCmsUi module + +The Magento_MediaGalleryCmsUi module provides Magento_Cms related UI elements to the media gallery user interface + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml new file mode 100644 index 0000000000000..f0938016d12f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml @@ -0,0 +1,30 @@ +<?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="FillOutCustomCMSPageContentActionGroup"> + <annotations> + <description>Fills out the Page details (Page Title, Content and URL Key)</description> + </annotations> + + <arguments> + <argument name="title" type="string"/> + <argument name="content" type="string"/> + <argument name="identifier" type="string"/> + </arguments> + + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{content}}" stepKey="fillFieldContentHeading"/> + <scrollTo selector="{{CmsNewPagePageContentSection.content}}" stepKey="scrollToPageContent"/> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{content}}" stepKey="fillFieldContent"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{identifier}}" stepKey="fillFieldUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml new file mode 100644 index 0000000000000..810d9eea4e261 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml @@ -0,0 +1,60 @@ +<?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="AdminMediaGalleryCmsUiUsedInBlocksFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInBlocksFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in blocks filter"/> + <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/4951850"/> + <description value="User filters assets used in blocks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="block" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="block" stepKey="deleteBlock"/> + </after> + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$block$$"/> + </actionGroup> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="saveBlock"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Blocks"/> + <argument name="optionName" value="$$block.title$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml new file mode 100644 index 0000000000000..a6bfdb781a734 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml @@ -0,0 +1,68 @@ +<?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="AdminMediaGalleryCmsUiUsedInPagesFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInPagesFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in pages filter"/> + <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/4934276"/> + <description value="User filters assets used in pages"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> + <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> + <argument name="title" value="Unique page title MediaGalleryUi"/> + <argument name="content" value="MediaGalleryUI content"/> + <argument name="identifier" value="test-page-1"/> + </actionGroup> + + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Pages"/> + <argument name="optionName" value="Unique page title MediaGalleryUi"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + + <actionGroup ref="AdminNavigateToPageGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="AdminSearchCmsPageInGridByUrlKeyActionGroup" stepKey="findCreatedCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json new file mode 100644 index 0000000000000..1ecfb9a3c8855 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-media-gallery-cms-ui", + "description": "Cms related UI elements in the magento media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCmsUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..b06ad0fff1df6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?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"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="page_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page</item> + <item name="block_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_page</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_block</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="cms_block" xsi:type="array"> + <item name="name" xsi:type="string">Blocks</item> + <item name="link" xsi:type="string">cms/block/index</item> + </item> + <item name="cms_page" xsi:type="array"> + <item name="name" xsi:type="string">Pages</item> + <item name="link" xsi:type="string">cms/page/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..2dc8b3ade5be7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?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:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_cms" frontName="media_gallery_cms"> + <module name="Magento_MediaGalleryCmsUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/module.xml b/app/code/Magento/MediaGalleryCmsUi/etc/module.xml new file mode 100644 index 0000000000000..8a39b8328b387 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/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_MediaGalleryCmsUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/registration.php b/app/code/Magento/MediaGalleryCmsUi/registration.php new file mode 100644 index 0000000000000..0e68935eba590 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCmsUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..509a7e6a53673 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..509a7e6a53673 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php new file mode 100644 index 0000000000000..317b811df5692 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Model; + +use Magento\Framework\DataObject; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Provider to get open media gallery dialog URL for WYSIWYG and widgets + */ +class OpenDialogUrlProvider extends DataObject +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @param ConfigInterface $config + */ + public function __construct(ConfigInterface $config) + { + $this->config = $config; + } + + /** + * Get Url based on media gallery configuration + * + * @return string + */ + public function getUrl(): string + { + return $this->config->isEnabled() ? 'media_gallery/index/index' : 'cms/wysiwyg_images/index'; + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php new file mode 100644 index 0000000000000..fbe35db298b04 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Uploader; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Psr\Log\LoggerInterface; + +/** + * Save image information by SaveAssetsInterface. + */ +class SaveImageInformation +{ + private const IMAGE_FILE_NAME_PATTERN = '#\.(jpg|jpeg|gif|png)$# i'; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @param Filesystem $filesystem + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param SynchronizeFilesInterface $synchronizeFiles + * @param ConfigInterface $config + */ + public function __construct( + Filesystem $filesystem, + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + SynchronizeFilesInterface $synchronizeFiles, + ConfigInterface $config + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->filesystem = $filesystem; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + } + + /** + * Saves asset to media gallery after save image. + * + * @param Uploader $subject + * @param array $result + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Uploader $subject, array $result): array + { + if (!$this->config->isEnabled()) { + return $result; + } + + $path = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath(rtrim($result['path'], '/') . '/' . ltrim($result['file'], '/')); + if (!$this->isApplicable($path)) { + return $result; + } + $this->synchronizeFiles->execute([$path]); + + return $result; + } + + /** + * Can asset be saved with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match(self::IMAGE_FILE_NAME_PATTERN, $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/README.md b/app/code/Magento/MediaGalleryIntegration/README.md new file mode 100644 index 0000000000000..365cde86777f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryIntegration + +The purpose of this module is to keep the integration of enhanced media gallery to Magento separated from implementation. diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php new file mode 100644 index 0000000000000..dfeaa3eff56bd --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Ui\Component\Form\Element\DataType\Media\Image; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class ImageComponentOpenDialogUrlTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Image + */ + private $image; + + /** + * @var string + */ + private $mediaGalleryOpenDialogUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->image = $this->objectManger->create(Image::class); + $this->image->setData('config', ['initialMediaGalleryOpenSubpath' => 'wysiwyg']); + + $url = $this->objectManger->create(UrlInterface::class); + $this->mediaGalleryOpenDialogUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertNotEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php new file mode 100644 index 0000000000000..7a3316f293879 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\MediaGalleryIntegration\Model\OpenDialogUrlProvider; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests cover getting correct url based on the config settings. + * @magentoAppArea adminhtml + */ +class OpenDialogUrlProviderTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var OpenDialogUrlProvider + */ + private $openDialogUrlProvider; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $config = $this->objectManger->create(ConfigInterface::class); + $this->openDialogUrlProvider = $this->objectManger->create( + OpenDialogUrlProvider::class, + ['config' => $config] + ); + } + + /** + * Test getting open dialog url with enhanced media gallery disabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + self::assertEquals('cms/wysiwyg_images/index', $this->openDialogUrlProvider->getUrl()); + } + + /** + * Test getting open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + self::assertEquals('media_gallery/index/index', $this->openDialogUrlProvider->getUrl()); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php new file mode 100644 index 0000000000000..81a4dc642cfa0 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tinymce3\Model\Config\Gallery\Config; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class TinyMceOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Config + */ + private $tinyMce3Config; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $fileBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->tinyMce3Config = $this->objectManger->create(Config::class); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $this->fileBrowserWindowUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertNotEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php new file mode 100644 index 0000000000000..aebf5927869d5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Config; +use Magento\Cms\Model\Wysiwyg\Gallery\DefaultConfigProvider; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update wysiwyg editor dialog url update when media gallery enabled. + * @magentoAppArea adminhtml + */ +class WysiwygDefaultConfigOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $filesBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $imageHelper = $this->objectManger->create(Images::class); + $this->filesBrowserWindowUrl = $url->getUrl( + 'media_gallery/index/index', + ['current_tree_path' => $imageHelper->idEncode(Config::IMAGE_DIRECTORY)] + ); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertNotEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json new file mode 100644 index 0000000000000..c55d6e0b89733 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -0,0 +1,31 @@ +{ + "name": "magento/module-media-gallery-integration", + "description": "Magento module responsible for integration of enhanced media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*" + }, + "require-dev": { + "magento/module-cms": "*" + }, + "suggest": { + "magento/module-catalog": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..d4b4f8988b622 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl"> + <arguments> + <argument name="url" xsi:type="object">Magento\MediaGalleryIntegration\Model\OpenDialogUrlProvider</argument> + </arguments> + </type> + <type name="Magento\Framework\File\Uploader"> + <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/etc/module.xml b/app/code/Magento/MediaGalleryIntegration/etc/module.xml new file mode 100644 index 0000000000000..88af90477cc8a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryIntegration"> + <sequence> + <module name="Magento_Ui"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/registration.php b/app/code/Magento/MediaGalleryIntegration/registration.php new file mode 100644 index 0000000000000..028f8d5b4288a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryIntegration', __DIR__); diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php new file mode 100644 index 0000000000000..9935904468388 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Write iptc data to the file return updated FileInterface with iptc data + */ +class AddIptcMetadata +{ + private const IPTC_TITLE_SEGMENT = '2#005'; + private const IPTC_DESCRIPTION_SEGMENT = '2#120'; + private const IPTC_KEYWORDS_SEGMENT = '2#025'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param DriverInterface $driver + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + DriverInterface $driver, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->driver = $driver; + $this->fileReader = $fileReader; + } + + /** + * Write metadata + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @param null|SegmentInterface $segment + */ + public function execute(FileInterface $file, MetadataInterface $metadata, ?SegmentInterface $segment): FileInterface + { + if (!is_callable('iptcembed') && !is_callable('iptcparse')) { + throw new LocalizedException(__('iptcembed() && iptcparse() must be enabled in php configuration')); + } + + $iptcData = $segment ? iptcparse($segment->getData()) : []; + + if ($metadata->getTitle() !== null) { + $iptcData[self::IPTC_TITLE_SEGMENT][0] = $metadata->getTitle(); + } + + if ($metadata->getDescription() !== null) { + $iptcData[self::IPTC_DESCRIPTION_SEGMENT][0] = $metadata->getDescription(); + } + + if ($metadata->getKeywords() !== null) { + $iptcData = $this->writeKeywords($metadata->getKeywords(), $iptcData); + } + + $newData = ''; + + foreach ($iptcData as $tag => $values) { + foreach ($values as $value) { + $newData .= $this->iptcMaketag(2, (int) substr($tag, 2), $value); + } + } + + $this->writeFile($file->getPath(), iptcembed($newData, $file->getPath())); + + $fileWithIptc = $this->fileReader->execute($file->getPath()); + + return $this->fileFactory->create([ + 'path' => $fileWithIptc->getPath(), + 'segments' => $this->getSegmentsWithIptc($fileWithIptc, $file) + ]); + } + + /** + * Return iptc segment from file. + * + * @param FileInterface $fileWithIptc + * @param FileInterface $originFile + */ + private function getSegmentsWithIptc(FileInterface $fileWithIptc, $originFile): array + { + $segments = $fileWithIptc->getSegments(); + $originFileSegments = $originFile->getSegments(); + + foreach ($segments as $key => $segment) { + if ($segment->getName() === 'APP13') { + foreach ($originFileSegments as $originKey => $segment) { + if ($segment->getName() === 'APP13') { + $originFileSegments[$originKey] = $segments[$key]; + } + } + return $originFileSegments; + } + } + return $originFileSegments; + } + + /** + * Write keywords field to the iptc segment. + * + * @param array $keywords + * @param array $iptcData + */ + private function writeKeywords(array $keywords, array $iptcData): array + { + foreach ($keywords as $key => $keyword) { + $iptcData[self::IPTC_KEYWORDS_SEGMENT][$key] = $keyword; + } + return $iptcData; + } + + /** + * Write iptc data to the image directly to the file. + * + * @param string $filePath + * @param string $content + */ + private function writeFile(string $filePath, string $content): void + { + $resource = $this->driver->fileOpen($filePath, 'wb'); + + $this->driver->fileWrite($resource, $content); + $this->driver->fileClose($resource); + } + + /** + * Create new iptc tag text + * + * @param int $rec + * @param int $tag + * @param string $value + */ + private function iptcMaketag(int $rec, int $tag, string $value) + { + //phpcs:disable Magento2.Functions.DiscouragedFunction + $length = strlen($value); + $retval = chr(0x1C) . chr($rec) . chr($tag); + + if ($length < 0x8000) { + $retval .= chr($length >> 8) . chr($length & 0xFF); + } else { + $retval .= chr(0x80) . + chr(0x04) . + chr(($length >> 24) & 0xFF) . + chr(($length >> 16) & 0xFF) . + chr(($length >> 8) & 0xFF) . + chr($length & 0xFF); + } + //phpcs:enable Magento2.Functions.DiscouragedFunction + return $retval . $value; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php new file mode 100644 index 0000000000000..269df146f2c81 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to the XMP template + */ +class AddXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag'; + private const XMP_XPATH_SELECTOR_KEYWORDS_EACH = '//dc:subject/rdf:Bag/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORD_ITEM = 'rdf:li'; + + /** + * Parse metadata + * + * @param string $data + * @param MetadataInterface $metadata + * @return string + */ + public function execute(string $data, MetadataInterface $metadata): string + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + if ($metadata->getTitle() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE, $metadata->getTitle()); + } + if ($metadata->getDescription() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION, $metadata->getDescription()); + } + if ($metadata->getKeywords() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_KEYWORDS); + } else { + $this->updateKeywords($xml, $metadata->getKeywords()); + } + + $data = $xml->asXML(); + return str_replace("<?xml version=\"1.0\"?>\n", '', $data); + } + + /** + * Update keywords + * + * @param \SimpleXMLElement $xml + * @param array $keywords + */ + private function updateKeywords(\SimpleXMLElement $xml, array $keywords): void + { + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS_EACH) as $keywordElement) { + unset($keywordElement[0]); + } + + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) as $element) { + foreach ($keywords as $keyword) { + $element->addChild(self::XMP_XPATH_SELECTOR_KEYWORD_ITEM, $keyword); + } + } + } + + /** + * Deletes xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + */ + private function deleteValueByXpath(\SimpleXMLElement $xml, string $xpath): void + { + foreach ($xml->xpath($xpath) as $element) { + unset($element[0]); + } + } + + /** + * Set value to xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + * @param string $value + */ + private function setValueByXpath(\SimpleXMLElement $xml, string $xpath, string $value): void + { + foreach ($xml->xpath($xpath) as $element) { + $element[0] = $value; + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File.php b/app/code/Magento/MediaGalleryMetadata/Model/File.php new file mode 100644 index 0000000000000..4b7605e8ec839 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; + +/** + * File internal data transfer object + */ +class File implements FileInterface +{ + /** + * @var string + */ + private $path; + + /** + * @var array + */ + private $segments; + + /** + * @var FileExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param string $path + * @param array $segments + * @param FileExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $path, + array $segments, + ?FileExtensionInterface $extensionAttributes = null + ) { + $this->path = $path; + $this->segments = $segments; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * @inheritdoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?FileExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php new file mode 100644 index 0000000000000..d5918781135a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Add metadata to the asset by path. Should be used as a virtual type with a file type specific configuration + */ +class AddMetadata implements AddMetadataInterface +{ + /** + * @var array + */ + private $segmentWriters; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var WriteFileInterface + */ + private $fileWriter; + + /** + * @param FileInterfaceFactory $fileFactory + * @param ReadFileInterface $fileReader + * @param WriteFileInterface $fileWriter + * @param array $segmentWriters + */ + public function __construct( + FileInterfaceFactory $fileFactory, + ReadFileInterface $fileReader, + WriteFileInterface $fileWriter, + array $segmentWriters + ) { + $this->fileFactory = $fileFactory; + $this->fileReader = $fileReader; + $this->fileWriter = $fileWriter; + $this->segmentWriters = $segmentWriters; + } + + /** + * @inheritdoc + */ + public function execute(string $path, MetadataInterface $metadata): void + { + try { + $file = $this->fileReader->execute($path); + } catch (ValidatorException $e) { + return; + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not parse the image file for metadata: %path', ['path' => $path]) + ); + } + + try { + $this->fileWriter->execute($this->writeMetadata($file, $metadata)); + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not update the image file metadata: %path', ['path' => $path]) + ); + } + } + + /** + * Write metadata by given metadata writer + * + * @param FileInterface $file + * @param MetadataInterface $metadata + */ + private function writeMetadata(FileInterface $file, MetadataInterface $metadata): FileInterface + { + foreach ($this->segmentWriters as $writer) { + if (!$writer instanceof WriteMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . WriteFileInterface::class) + ); + } + + $file = $writer->execute($file, $metadata); + } + return $file; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php new file mode 100644 index 0000000000000..d9a8202281fff --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; + +/** + * Extract Metadata from asset file by given extractors + */ +class ExtractMetadata implements ExtractMetadataInterface +{ + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var array + */ + private $segmentReaders; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param MetadataInterfaceFactory $metadataFactory + * @param ReadFileInterface $fileReader + * @param array $segmentReaders + */ + public function __construct( + FileInterfaceFactory $fileFactory, + MetadataInterfaceFactory $metadataFactory, + ReadFileInterface $fileReader, + array $segmentReaders + ) { + $this->fileFactory = $fileFactory; + $this->metadataFactory = $metadataFactory; + $this->fileReader = $fileReader; + $this->segmentReaders = $segmentReaders; + } + + /** + * @inheritdoc + */ + public function execute(string $path): MetadataInterface + { + try { + return $this->extractMetadata($path); + } catch (\Exception $exception) { + return $this->metadataFactory->create(); + } + } + + /** + * Extract metadata from file + * + * @param string $path + * @return MetadataInterface + */ + private function extractMetadata(string $path): MetadataInterface + { + try { + $file = $this->fileReader->execute($path); + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not parse the image file for metadata: %path', ['path' => $path]) + ); + } + + return $this->readSegments($file); + } + + /** + * Read file segments by given segmentReader + * + * @param FileInterface $file + */ + private function readSegments(FileInterface $file): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->segmentReaders as $segmentReader) { + if (!$segmentReader instanceof ReadMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($segmentReader) . ' must implement ' . ReadMetadataInterface::class) + ); + } + + $data = $segmentReader->execute($file); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php new file mode 100644 index 0000000000000..d7290f31ee34e --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Get metadata from IPTC block + */ +class GetIptcMetadata +{ + private const IPTC_TITLE = '2#005'; + private const IPTC_DESCRIPTION = '2#120'; + private const IPTC_KEYWORDS = '2#025'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $title = ''; + $description = ''; + $keywords = []; + + if (is_callable('iptcparse')) { + $iptcData = iptcparse($data); + + if (!empty($iptcData[self::IPTC_TITLE])) { + $title = trim($iptcData[self::IPTC_TITLE][0]); + } + + if (!empty($iptcData[self::IPTC_DESCRIPTION][0])) { + $description = trim($iptcData[self::IPTC_DESCRIPTION][0]); + } + + if (!empty($iptcData[self::IPTC_KEYWORDS][0])) { + $keywords = array_values($iptcData[self::IPTC_KEYWORDS]); + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php new file mode 100644 index 0000000000000..bda01645ddfec --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; + +/** + * Get metadata from XMP block + */ +class GetXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag/rdf:li'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct(MetadataInterfaceFactory $metadataFactory) + { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + $keywords = array_map( + function (\SimpleXMLElement $element): string { + return (string) $element; + }, + $xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) + ); + + $description = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_DESCRIPTION)); + $title = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_TITLE)); + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php new file mode 100644 index 0000000000000..88810d3ccf28f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php @@ -0,0 +1,318 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + + $header = $this->read($resource, 3); + + if ($header != "GIF") { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a GIF image')); + } + + $version = $this->read($resource, 3); + + if (!in_array($version, ['87a', '89a'])) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('Unexpected GIF version')); + } + + $headerSegment = $this->segmentFactory->create([ + 'name' => 'header', + 'data' => $header . $version + ]); + + $width = $this->read($resource, 2); + $height = $this->read($resource, 2); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $backgroundAndAspectRatio = $this->read($resource, 2); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + + $generalSegment = $this->segmentFactory->create([ + 'name' => 'header2', + 'data' => $width . $height . $bitPerPixelBinary . $backgroundAndAspectRatio . $globalColorTable + ]); + + $segments = $this->getSegments($resource); + + array_unshift($segments, $headerSegment, $generalSegment); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read gif segments + * + * @param resource $resource + * @return SegmentInterface[] + * @throws FileSystemException + */ + private function getSegments($resource): array + { + $gifFrameSeparator = pack("C", ord(",")); + $gifExtensionSeparator = pack("C", ord("!")); + $gifTerminator = pack("C", ord(";")); + + $segments = []; + do { + $separator = $this->read($resource, 1); + + if ($separator == $gifTerminator) { + return $segments; + } + + if ($separator == $gifFrameSeparator) { + $segments[] = $this->segmentFactory->create([ + 'name' => 'frame', + 'data' => $gifFrameSeparator . $this->readFrame($resource) + ]); + continue; + } + + if ($separator != $gifExtensionSeparator) { + throw new LocalizedException(__('The file is corrupted')); + } + + $segments[] = $this->getExtensionSegment($resource); + } while (!$this->driver->endOfFile($resource)); + + return $segments; + } + + /** + * Read extension segment + * + * @param resource $resource + * @return SegmentInterface + * @throws FileSystemException + */ + private function getExtensionSegment($resource): SegmentInterface + { + $gifExtensionSeparator = pack("C", ord("!")); + $extensionCodeBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $extensionCode = unpack('C', $extensionCodeBinary)[1]; + + if ($extensionCode == 0xF9) { + return $this->segmentFactory->create([ + 'name' => 'Graphics Control Extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode == 0xFE) { + return $this->segmentFactory->create([ + 'name' => 'comment', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode != 0xFF) { + return $this->segmentFactory->create([ + 'name' => 'Programm extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + $blockLengthBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $blockLength = unpack('C', $blockLengthBinary)[1]; + $name = $this->read($resource, $blockLength); + + if ($blockLength != 11) { + throw new LocalizedException(__('The file is corrupted')); + } + + if ($name == 'XMP DataXMP') { + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlockWithSubblocks($resource) + ]); + } + + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlock($resource) + ]); + } + + /** + * Read gif frame + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readFrame($resource): string + { + $boundingBox = $this->read($resource, 8); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + return $boundingBox . $bitPerPixelBinary . $globalColorTable . $this->read($resource, 1) + . $this->readBlockWithSubblocks($resource); + } + + /** + * Retrieve bits per pixel value + * + * @param string $data + * @return int + */ + private function getBitPerPixel(string $data): int + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $bitPerPixel = unpack('C', $data)[1]; + $bpp = ($bitPerPixel & 7) + 1; + $bitPerPixel >>= 7; + $haveMap = $bitPerPixel & 1; + return $haveMap ? $bpp : 0; + } + + /** + * Read global color table + * + * @param resource $resource + * @param int $bitPerPixel + * @return string + * @throws FileSystemException + */ + private function getGlobalColorTable($resource, int $bitPerPixel): string + { + $globalColorTable = ''; + if ($bitPerPixel > 0) { + $max = pow(2, $bitPerPixel); + for ($i = 1; $i <= $max; ++$i) { + $globalColorTable .= $this->read($resource, 3); + } + } + return $globalColorTable; + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } + + /** + * Read the block stored in multiple sections + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readBlockWithSubblocks($resource): string + { + $data = ''; + $subLength = $this->read($resource, 1); + + while ($subLength !== "\0") { + $data .= $subLength . $this->read($resource, ord($subLength)); + $subLength = $this->read($resource, 1); + } + + return $data . $subLength; + } + + /** + * Read gif block + * + * @param resource $resource + * @return string + * @throws FileSystemException] + */ + private function readBlock($resource): string + { + $blockLengthBinary = $this->read($resource, 1); + $blockLength = ord($blockLengthBinary); + if ($blockLength == 0) { + return ''; + } + return $blockLengthBinary . $this->read($resource, $blockLength) . $this->read($resource, 1); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php new file mode 100644 index 0000000000000..1b83554ef4df3 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * XMP Reader for gif file format + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + /** + * see XMP Specification Part 3, 1.1.2 GIF + */ + private const MAGIC_TRAILER_LENGTH = 258; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + $xmp = substr($segment->getData(), 14); + + if (substr($xmp, -self::MAGIC_TRAILER_LENGTH, 3) !== self::MAGIC_TRAILER_START + || substr($xmp, -5) !== self::MAGIC_TRAILER_END + ) { + throw new LocalizedException(__('XMP data is corrupted')); + } + + return substr($xmp, 0, -self::MAGIC_TRAILER_LENGTH); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php new file mode 100644 index 0000000000000..2b5167eba596b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php @@ -0,0 +1,191 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for GIF format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + private const XMP_DATA_START_POSITION = 14; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $gifSegments = $file->getSegments(); + $xmpGifSegments = []; + foreach ($gifSegments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpGifSegments[$key] = $segment; + } + } + + if (empty($xmpGifSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpGifSegment($gifSegments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpGifSegments as $key => $segment) { + $gifSegments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $gifSegments + ]); + } + + /** + * Insert XMP segment to gif image segments (at position 3) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpGifSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 4), [$xmpSegment], array_slice($segments, 4)); + } + + /** + * Return XMP template from string + * + * @param string $string + * @param string $start + * @param string $end + */ + private function getXmpData(string $string, string $start, string $end): string + { + $string = ' ' . $string; + $ini = strpos($string, $start); + if ($ini == 0) { + return ''; + } + $ini += strlen($start); + $len = strpos($string, $end, $ini) - $ini; + + return substr($string, $ini, $len); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + + $xmpSegment = pack("C", ord("!")) . pack("C", 255) . pack("C", 11) . + self::XMP_SEGMENT_NAME . $this->addXmpMetadata->execute($xmpData, $metadata) . "\x01"; + + /** + * Write Magic trailer 258 bytes see XMP Specification Part 3, 1.1.2 GIF + */ + $i = 255; + while ($i > 0) { + $xmpSegment .= pack("C", $i); + $i--; + } + + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => $xmpSegment . "\0\0" + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = $this->getXmpData($data, self::XMP_SEGMENT_NAME, "\x01"); + $end = substr($data, strpos($data, "\x01")); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) . $end + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php new file mode 100644 index 0000000000000..cbdc9fa286e85 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments writer + */ +class WriteFile implements WriteFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write gif segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite( + $resource, + $segment->getData() + ); + } + $this->driver->fileWrite($resource, pack("C", ord(";"))); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php new file mode 100644 index 0000000000000..4dbff068a861b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; + +/** + * Jpeg file reader + */ +class ReadFile implements ReadFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + private const MARKER_IMAGE_START = "\xDA"; + + private const TWO_BYTES = 2; + private const ONE_MEGABYTE = 1048576; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * Is reader applicable + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + public function isApplicable(string $path): bool + { + $resource = $this->driver->fileOpen($path, 'rb'); + try { + $marker = $this->readMarker($resource); + } catch (LocalizedException $exception) { + return false; + } + $this->driver->fileClose($resource); + + return $marker == self::MARKER_IMAGE_FILE_START; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + if (!$this->isApplicable($path)) { + throw new ValidatorException(__('Not a JPEG image')); + } + + $resource = $this->driver->fileOpen($path, 'rb'); + $marker = $this->readMarker($resource); + + if ($marker != self::MARKER_IMAGE_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a JPEG image')); + } + + do { + $marker = $this->readMarker($resource); + $segments[] = $this->readSegment($resource, ord($marker)); + } while (($marker != self::MARKER_IMAGE_START) && (!$this->driver->endOfFile($resource))); + + if ($marker != self::MARKER_IMAGE_START) { + throw new LocalizedException(__('File is corrupted')); + } + + $segments[] = $this->segmentFactory->create([ + 'name' => 'CompressedImage', + 'data' => $this->readCompressedImage($resource) + ]); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read jpeg marker + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readMarker($resource): string + { + $data = $this->read($resource, self::TWO_BYTES); + + if ($data[0] != self::MARKER_PREFIX) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('File is corrupted')); + } + + return $data[1]; + } + + /** + * Read compressed image + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readCompressedImage($resource): string + { + $compressedImage = ''; + do { + $compressedImage .= $this->read($resource, self::ONE_MEGABYTE); + } while (!$this->driver->endOfFile($resource)); + + $endOfImageMarkerPosition = strpos($compressedImage, self::MARKER_PREFIX . self::MARKER_IMAGE_END); + + if ($endOfImageMarkerPosition !== false) { + $compressedImage = substr($compressedImage, 0, $endOfImageMarkerPosition); + } + + return $compressedImage; + } + + /** + * Read jpeg segment + * + * @param resource $resource + * @param int $segmentType + * @return SegmentInterface + * @throws FileSystemException + */ + private function readSegment($resource, int $segmentType): SegmentInterface + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentSize = unpack('nsize', $this->read($resource, 2))['size'] - 2; + return $this->segmentFactory->create([ + 'name' => $this->segmentNames->getSegmentName($segmentType), + 'data' => $this->read($resource, $segmentSize) + ]); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php new file mode 100644 index 0000000000000..94ccb400e5e0a --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetIptcMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for jpeg image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetIptcMetadata + */ + private $getIptcData; + + /** + * @param GetIptcMetadata $getIptcData + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + GetIptcMetadata $getIptcData, + MetadataInterfaceFactory $metadataFactory + ) { + $this->getIptcData = $getIptcData; + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + return $this->getIptcData->execute($segment->getData()); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php new file mode 100644 index 0000000000000..81ff7200c3475 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Jpeg XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isSegmentXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), self::XMP_DATA_START_POSITION); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php new file mode 100644 index 0000000000000..e9fcd500f1dca --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddIptcMetadata; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg IPTC Writer + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0\0x00'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddIPtcMetadata + */ + private $addIptcMetadata; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddIptcMetadata $addIptcMetadata + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddIptcMetadata $addIptcMetadata, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addIptcMetadata = $addIptcMetadata; + $this->fileReader = $fileReader; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $iptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $iptcSegments[$key] = $segment; + } + } + + foreach ($iptcSegments as $segment) { + return $this->addIptcMetadata->execute($file, $metadata, $segment); + } + return $this->addIptcMetadata->execute($file, $metadata, null); + } + + /** + * Check if segment contains IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php new file mode 100644 index 0000000000000..e88cdd5b7b8f4 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg XMP Writer + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $xmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpSegments[$key] = $segment; + } + } + + if (empty($xmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpSegment($segments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = substr($data, self::XMP_DATA_START_POSITION); + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php new file mode 100644 index 0000000000000..403bc7f3d7449 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_IMAGE_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + foreach ($file->getSegments() as $segment) { + if ($segment->getName() != 'CompressedImage' && strlen($segment->getData()) > 0xfffd) { + throw new LocalizedException(__('A Header is too large to fit in the segment!')); + } + } + + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_END); + $this->driver->fileClose($resource); + } + + /** + * Write jpeg segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + if ($segment->getName() !== 'CompressedImage') { + $this->driver->fileWrite( + $resource, + //phpcs:ignore Magento2.Functions.DiscouragedFunction + self::MARKER_IMAGE_PREFIX . chr($this->segmentNames->getSegmentType($segment->getName())) + ); + $this->driver->fileWrite($resource, pack("n", strlen($segment->getData()) + 2)); + } + $this->driver->fileWrite($resource, $segment->getData()); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php new file mode 100644 index 0000000000000..9e3ee5d29a495 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Media asset metadata data transfer object + */ +class Metadata implements MetadataInterface +{ + /** + * @var string + */ + private $title; + + /** + * @var string + */ + private $description; + + /** + * @var array + */ + private $keywords; + + /** + * @var MetadataExtensionInterface + */ + private $extensionAttributes; + + /** + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @param MetadataExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $title = null, + string $description = null, + array $keywords = null, + ?MetadataExtensionInterface $extensionAttributes = null + ) { + $this->title = $title; + $this->description = $description; + $this->keywords = $keywords; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getTitle(): ?string + { + return $this->title; + } + + /** + * @inheritdoc + */ + public function getKeywords(): ?array + { + return $this->keywords; + } + + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php new file mode 100644 index 0000000000000..673f8ff436ebe --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + private const PNG_MARKER_IMAGE_END = 'IEND'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + $header = $this->readHeader($resource); + + if ($header != self::PNG_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a PNG image')); + } + + do { + $header = $this->readHeader($resource); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentHeader = unpack('Nsize/a4type', $header); + $data = $this->read($resource, $segmentHeader['size']); + $segments[] = $this->segmentFactory->create([ + 'name' => $segmentHeader['type'], + 'data' => $data + ]); + $cyclicRedundancyCheck = $this->read($resource, 4); + + if (pack('N', crc32($segmentHeader['type'] . $data)) != $cyclicRedundancyCheck) { + throw new LocalizedException(__('The image is corrupted')); + } + } while ($header + && $segmentHeader['type'] != self::PNG_MARKER_IMAGE_END + && !$this->driver->endOfFile($resource) + ); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read 8 bytes + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readHeader($resource): string + { + return $this->read($resource, 8); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php new file mode 100644 index 0000000000000..c856d95475a40 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for png image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_CHUNK_MARKER_LENGTH = 4; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + return $this->getIptcData($segment); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Read iptc data from zTXt segment + * + * @param SegmentInterface $segment + */ + private function getIptcData(SegmentInterface $segment): MetadataInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker); + if ($descriptionStartPosition) { + $description = substr( + $binData, + $descriptionStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $descriptionStartPosition + 3, 1)) + ); + } + + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker); + if ($titleStartPosition) { + $title = substr( + $binData, + $titleStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $titleStartPosition + 3, 1)) + ); + } + + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker); + if ($keywordsStartPosition) { + $keywords = substr( + $binData, + $keywordsStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $keywordsStartPosition + 3, 1)) + ); + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? explode(',', $keywords) : null + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php new file mode 100644 index 0000000000000..83ba554f7bf5d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * PNG XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmpSegment($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php new file mode 100644 index 0000000000000..d40dbc13d2962 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * IPTC Writer to write IPTC data for png image + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_SEGMENT_START_STRING = 'Raw profile type iptc'; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * Write iptc metadata to zTXt segment + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngIptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $pngIptcSegments[$key] = $segment; + } + } + + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + + if (empty($pngIptcSegments)) { + $segments[] = $this->createPngIptcSegment($metadata); + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + foreach ($pngIptcSegments as $key => $segment) { + $segments[$key] = $this->updateIptcSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Create new zTXt segment with metadata + * + * @param MetadataInterface $metadata + */ + private function createPngIptcSegment(MetadataInterface $metadata): SegmentInterface + { + $start = '8BIM' . str_repeat(pack('C', 4), 2) . str_repeat(pack("C", 0), 5) + . 'c' . pack('C', 28) . pack('C', 1); + $compression = 'Z' . pack('C', 0) . pack('C', 3) . pack('C', 27) . '%G' . pack('C', 28) . pack('C', 1); + $end = str_repeat(pack('C', 0), 2) . pack('C', 2) . pack('C', 0) . pack('C', 4) . pack('C', 28); + $binData = $start . $compression . $end; + + $description = $metadata->getDescription(); + if ($description !== null) { + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $binData .= $descriptionMarker . pack('C', strlen($description)) . $description . pack('C', 28); + } + + $title = $metadata->getTitle(); + if ($title !== null) { + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $binData .= $titleMarker . pack('C', strlen($title)) . $title . pack('C', 28); + } + + $keywords = $metadata->getKeywords(); + if ($keywords !== null) { + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywords = implode(',', $keywords); + $binData .= $keywordsMarker . pack('C', strlen($keywords)) . $keywords . pack('C', 28); + } + + $binData .= pack('C', 0); + $hexString = bin2hex($binData); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $compressedIptcData = gzcompress(PHP_EOL . 'iptc' . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => self::IPTC_SEGMENT_NAME, + 'data' => self::IPTC_SEGMENT_START_STRING . str_repeat(pack('C', 0), 2) . $compressedIptcData + ]); + } + + /** + * Update iptc data to zTXt segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + */ + private function updateIptcSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + if ($metadata->getDescription() !== null) { + $description = $metadata->getDescription(); + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($description)) . $description, + $descriptionStartPosition + ) . substr($binData, $descriptionStartPosition + 1 + ord(substr($binData, $descriptionStartPosition))); + } + + if ($metadata->getTitle() !== null) { + $title = $metadata->getTitle(); + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($title)) . $title, + $titleStartPosition + ) . substr($binData, $titleStartPosition + 1 + ord(substr($binData, $titleStartPosition))); + } + + if ($metadata->getKeywords() !== null) { + $keywords = implode(',', $metadata->getKeywords()); + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($keywords)) . $keywords, + $keywordsStartPosition + ) . substr($binData, $keywordsStartPosition + 1 + ord(substr($binData, $keywordsStartPosition))); + } + $hexString = bin2hex($binData); + $iptcSegmentStart = substr($segment->getData(), 0, $iptSegmentStartPosition + 2); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentDataCompressed = gzcompress(PHP_EOL . $data[0] . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $iptcSegmentStart . $segmentDataCompressed + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php new file mode 100644 index 0000000000000..9d8d5d975d99d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for png format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + private const XMP_SEGMENT_START = "XML:com.adobe.xmp\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add xmp metadata to the png file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngXmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isXmpSegment($segment)) { + $pngXmpSegments[$key] = $segment; + } + } + + if (empty($pngXmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertPngXmpSegment($segments, $this->createPngXmpSegment($metadata)) + ]); + } + + foreach ($pngXmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image png segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertPngXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new png segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function createPngXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the png xmp segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($this->getXmpData($segment), $metadata) + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php new file mode 100644 index 0000000000000..c5db6644b3545 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write PNG file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::PNG_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write PNG segments + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite($resource, pack("N", strlen($segment->getData()))); + $this->driver->fileWrite($resource, pack("a4", $segment->getName())); + $this->driver->fileWrite($resource, $segment->getData()); + $this->driver->fileWrite($resource, pack("N", crc32($segment->getName() . $segment->getData()))); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Segment.php b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php new file mode 100644 index 0000000000000..0e8a89767e40c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Segment internal data transfer object + */ +class Segment implements SegmentInterface +{ + /** + * @var array + */ + private $name; + + /** + * @var string + */ + private $data; + + /** + * @var SegmentExtensionInterface + */ + private $extensionAttributes; + + /** + * @param string $name + * @param string $data + * @param SegmentExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $name, + string $data, + ?SegmentExtensionInterface $extensionAttributes = null + ) { + $this->name = $name; + $this->data = $data; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritdoc + */ + public function getData(): string + { + return $this->data; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php new file mode 100644 index 0000000000000..62eea09453ae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +/** + * Segment types to names mapper + */ +class SegmentNames +{ + private const SEGMENT_TYPE_TO_NAME = [ + 0xC0 => "SOF0", + 0xC1 => "SOF1", + 0xC2 => "SOF2", + 0xC3 => "SOF4", + 0xC5 => "SOF5", + 0xC6 => "SOF6", + 0xC7 => "SOF7", + 0xC8 => "JPG", + 0xC9 => "SOF9", + 0xCA => "SOF10", + 0xCB => "SOF11", + 0xCD => "SOF13", + 0xCE => "SOF14", + 0xCF => "SOF15", + 0xC4 => "DHT", + 0xCC => "DAC", + 0xD0 => "RST0", + 0xD1 => "RST1", + 0xD2 => "RST2", + 0xD3 => "RST3", + 0xD4 => "RST4", + 0xD5 => "RST5", + 0xD6 => "RST6", + 0xD7 => "RST7", + 0xD8 => "SOI", + 0xD9 => "EOI", + 0xDA => "SOS", + 0xDB => "DQT", + 0xDC => "DNL", + 0xDD => "DRI", + 0xDE => "DHP", + 0xDF => "EXP", + 0xE0 => "APP0", + 0xE1 => "APP1", + 0xE2 => "APP2", + 0xE3 => "APP3", + 0xE4 => "APP4", + 0xE5 => "APP5", + 0xE6 => "APP6", + 0xE7 => "APP7", + 0xE8 => "APP8", + 0xE9 => "APP9", + 0xEA => "APP10", + 0xEB => "APP11", + 0xEC => "APP12", + 0xED => "APP13", + 0xEE => "APP14", + 0xEF => "APP15", + 0xF0 => "JPG0", + 0xF1 => "JPG1", + 0xF2 => "JPG2", + 0xF3 => "JPG3", + 0xF4 => "JPG4", + 0xF5 => "JPG5", + 0xF6 => "JPG6", + 0xF7 => "JPG7", + 0xF8 => "JPG8", + 0xF9 => "JPG9", + 0xFA => "JPG10", + 0xFB => "JPG11", + 0xFC => "JPG12", + 0xFD => "JPG13", + 0xFE => "COM", + 0x01 => "TEM", + 0x02 => "RES", + ]; + + /** + * Get segment name by type + * + * @param int $type + * @return string + */ + public function getSegmentName(int $type): string + { + return self::SEGMENT_TYPE_TO_NAME[$type]; + } + + /** + * Get segment type by name + * + * @param string $name + * @return int + */ + public function getSegmentType(string $name): int + { + return array_search($name, self::SEGMENT_TYPE_TO_NAME); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php new file mode 100644 index 0000000000000..a7d07f66ba8aa --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Module\Dir; +use Magento\Framework\Module\Dir\Reader; + +/** + * XMP template provider + */ +class XmpTemplate +{ + private const XMP_TEMPLATE_FILENAME = 'default.xmp'; + + /** + * @var Reader + */ + private $moduleReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @param Reader $moduleReader + * @param DriverInterface $driver + */ + public function __construct(Reader $moduleReader, DriverInterface $driver) + { + $this->moduleReader = $moduleReader; + $this->driver = $driver; + } + + /** + * Get default XMP template + * + * @return string + * @throws FileSystemException + */ + public function get(): string + { + $etcDirectoryPath = $this->moduleReader->getModuleDir( + Dir::MODULE_ETC_DIR, + 'Magento_MediaGalleryMetadata' + ); + return $this->driver->fileGetContents( + $etcDirectoryPath . '/' . self::XMP_TEMPLATE_FILENAME + ); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/README.md b/app/code/Magento/MediaGalleryMetadata/README.md new file mode 100644 index 0000000000000..ec74e527ddebb --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadata + +The purpose of this module is to provide an ability to extract the metadata from file and populating Media Asset entity fields when an image is uploaded to Magento and also provide an ability to update the metadata stored in an image file. diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php new file mode 100644 index 0000000000000..c284bf71e60af --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php @@ -0,0 +1,197 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * ExtractMetadata test + */ +class AddMetadataTest extends TestCase +{ + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->addMetadata = Bootstrap::getObjectManager()->get(AddMetadataInterface::class); + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataInterfaceFactory::class); + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param null|string $fileName + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws LocalizedException + */ + public function testExecute( + ?string $fileName, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $metadata = $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + + $this->addMetadata->execute($modifiableFilePath, $metadata); + + $updatedMetadata = $this->extractMetadata->execute($modifiableFilePath); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'iptc_only.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + null, + null + ], + [ + 'iptc_only.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_iptc.jpeg', + 'Updated Title', + null, + null + ], + [ + 'macos-preview.png', + 'Title of the magento image 2', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ], + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'empty_xmp_image.png', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php new file mode 100644 index 0000000000000..982ccbb20fe2c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for ExtractMetadata + */ +class ExtractMetadataTest extends TestCase +{ + /** + * @var ExtractMetadataComposite + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testExecute( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $metadata = $this->extractMetadata->execute($path); + + $this->assertEquals($title, $metadata->getTitle()); + $this->assertEquals($description, $metadata->getDescription()); + $this->assertEquals($keywords, $metadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'macos-photos.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'macos-preview.png', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'exiftool.gif', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.png', + 'Title of the magento image', + 'PNG format is awesome', + [ + 'png', + 'awesome' + ] + ], + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php new file mode 100644 index 0000000000000..4bba73e3ca2a9 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Gif\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer gif format + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteReadGif( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalGifMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalGifMetadata->getTitle()); + $this->assertEmpty($originalGifMetadata->getDescription()); + $this->assertEmpty($originalGifMetadata->getKeywords()); + $updatedGifFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedGifMetadata = $this->xmpReader->execute($updatedGifFile); + $this->assertEquals($title, $updatedGifMetadata->getTitle()); + $this->assertEquals($description, $updatedGifMetadata->getDescription()); + $this->assertEquals($keywords, $updatedGifMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_exiftool.gif', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php new file mode 100644 index 0000000000000..932b71df28430 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php new file mode 100644 index 0000000000000..043e26f67853f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + $updatedFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedMetadata = $this->xmpReader->execute($updatedFile); + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php new file mode 100644 index 0000000000000..d8bcfd7a94561 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Png\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Png\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif new file mode 100644 index 0000000000000000000000000000000000000000..14cc6026b5950c8b2546c198467dc59dc31c1724 GIT binary patch literal 7951 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 F{{RBl3$y?L literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..144a56dac2d3e748d47cf56e0fdde0bab2f49def GIT binary patch literal 19416 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 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png new file mode 100644 index 0000000000000000000000000000000000000000..129c49a1b7e64148b223123349fc21f2dc1393c8 GIT binary patch literal 47596 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 HP00TMZ+Z(Z literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cee7bff38a6c62b9bb8d59b682b10567c6c696da GIT binary patch literal 19336 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 Y_Wa?0f${ujYsB!kdAI-N$2$K10F^uY0RR91 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png new file mode 100644 index 0000000000000000000000000000000000000000..7e81891ebc0eee3059e8e5455158993a0aa839fe GIT binary patch literal 55268 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 FzX0#`%BcVV literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif new file mode 100644 index 0000000000000000000000000000000000000000..70574d70b609e0a180bbf680447f6e970f484b43 GIT binary patch literal 12704 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 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5d7dba35fede7547ce742ea33ce62cc71d5f825f GIT binary patch literal 19884 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 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png new file mode 100644 index 0000000000000000000000000000000000000000..9b4821c1c4e5d755df65d09450bcb2b5c925e812 GIT binary patch literal 47894 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 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3a07b6abe788e83cc336eb8b198556c46e7ec911 GIT binary patch literal 22795 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 L<aM`$-mNzQDffZL literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..966520f0d01124eec138a9e29298dbe8e2fea5ab GIT binary patch literal 57535 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 V<L%D>xo;2f)6+51CZ4mo`(IOuD**ri literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/composer.json b/app/code/Magento/MediaGalleryMetadata/composer.json new file mode 100644 index 0000000000000..c2ce66ce64c36 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-media-gallery-metadata", + "description": "Magento module responsible for images metadata processing", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-metadata-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadata\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/etc/default.xmp b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp new file mode 100644 index 0000000000000..772b6af671ec6 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp @@ -0,0 +1,24 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> +<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0"> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description rdf:about="" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"> + <dc:subject> + <rdf:Seq> + <rdf:li>magento</rdf:li> + </rdf:Seq> + </dc:subject> + <dc:description><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:description> + <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:title> + <dc:subject> + <rdf:Bag> + <rdf:li>magento</rdf:li> + <rdf:li>mediagallerymetadata</rdf:li> + </rdf:Bag> + </dc:subject> + </rdf:Description> + </rdf:RDF> +</x:xmpmeta> +<?xpacket end="w"?> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/etc/di.xml b/app/code/Magento/MediaGalleryMetadata/etc/di.xml new file mode 100644 index 0000000000000..d2f1f90510488 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/di.xml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface" type="Magento\MediaGalleryMetadata\Model\Metadata"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\FileInterface" type="Magento\MediaGalleryMetadata\Model\File"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\SegmentInterface" type="Magento\MediaGalleryMetadata\Model\Segment"/> + <type name="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"> + <arguments> + <argument name="writers" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\AddMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\AddMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\XmpTemplate"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\AddIptcMetadata"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/etc/module.xml b/app/code/Magento/MediaGalleryMetadata/etc/module.xml new file mode 100644 index 0000000000000..776b05aecd284 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/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_MediaGalleryMetadata"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/registration.php b/app/code/Magento/MediaGalleryMetadata/registration.php new file mode 100644 index 0000000000000..fcf6789d9321f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryMetadata', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php new file mode 100644 index 0000000000000..df645681e8971 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to asset file + */ +interface AddMetadataInterface +{ + /** + * Add metadata to the asset file + * + * @param string $path + * @param MetadataInterface $metadata + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $metadata): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php new file mode 100644 index 0000000000000..63e943150f4a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; + +/** + * Media asset metadata data transfer object + */ +interface MetadataInterface extends ExtensibleDataInterface +{ + /** + * Get asset title + * + * @return null|string + */ + public function getTitle(): ?string; + + /** + * Get asset description + * + * @return null|string + */ + public function getDescription(): ?string; + + /** + * Get asset keywords + * + * @return null|array + */ + public function getKeywords(): ?array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php new file mode 100644 index 0000000000000..2327406db8bef --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Extract asset metadata + */ +interface ExtractMetadataInterface +{ + /** + * Extract metadata from the asset file + * + * @param string $path + * @return MetadataInterface + */ + public function execute(string $path): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php new file mode 100644 index 0000000000000..fc3f53313199d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer pool + */ +class AddMetadataComposite implements AddMetadataInterface +{ + /** + * @var AddMetadataInterface[] + */ + private $writers; + + /** + * @param AddMetadataInterface[] $writers + */ + public function __construct(array $writers) + { + $this->writers = $writers; + } + + /** + * Write metadata to the path + * + * @param string $path + * @param MetadataInterface $data + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $data): void + { + foreach ($this->writers as $writer) { + if (!$writer instanceof AddMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . AddMetadataInterface::class) + ); + } + + $writer->execute($path, $data); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php new file mode 100644 index 0000000000000..0d6e8aa345178 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; + +/** + * Metadata extractor composite + */ +class ExtractMetadataComposite implements ExtractMetadataInterface +{ + /** + * @var ExtractMetadataInterface[] + */ + private $extractors; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param ExtractMetadataInterface[] $extractors + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory, + array $extractors + ) { + $this->metadataFactory = $metadataFactory; + $this->extractors = $extractors; + } + + /** + * Extract metadata from file + * + * @param string $path + * @return MetadataInterface + * @throws LocalizedException + */ + public function execute(string $path): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->extractors as $extractor) { + if (!$extractor instanceof ExtractMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($extractor) . ' must implement ' . ExtractMetadataInterface::class) + ); + } + + $data = $extractor->execute($path); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php new file mode 100644 index 0000000000000..0cd01bbf57c64 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; + +/** + * File internal data transfer object + */ +interface FileInterface extends ExtensibleDataInterface +{ + /** + * Get file path + * + * @return string + */ + public function getPath(): string; + + /** + * Get metadata sections + * + * @return SegmentInterface[] + */ + public function getSegments(): array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null + */ + public function getExtensionAttributes(): ?FileExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php new file mode 100644 index 0000000000000..e45a934f7b5ad --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +/** + * File reader + */ +interface ReadFileInterface +{ + /** + * Create file object from the file + * + * @param string $path + * @return FileInterface + */ + public function execute(string $path): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php new file mode 100644 index 0000000000000..b6d97118f848b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata reader + */ +interface ReadMetadataInterface +{ + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php new file mode 100644 index 0000000000000..bf6cdc30306f8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; + +/** + * Segment internal data transfer object + */ +interface SegmentInterface extends ExtensibleDataInterface +{ + /** + * Get segment name + * + * @return string + */ + public function getName(): string; + + /** + * Get segment data + * + * @return string + */ + public function getData(): string; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php new file mode 100644 index 0000000000000..fe7579989c40f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; + +/** + * File writer + */ +interface WriteFileInterface +{ + /** + * Write file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php new file mode 100644 index 0000000000000..943879ebaec86 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer + */ +interface WriteMetadataInterface +{ + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $data + */ + public function execute(FileInterface $file, MetadataInterface $data): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/README.md b/app/code/Magento/MediaGalleryMetadataApi/README.md new file mode 100644 index 0000000000000..82f86d2f61c6d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadataApi + +The Magento_MediaGalleryMetadataApi module is responsible for the media gallery metadata implementation API. diff --git a/app/code/Magento/MediaGalleryMetadataApi/composer.json b/app/code/Magento/MediaGalleryMetadataApi/composer.json new file mode 100644 index 0000000000000..f8673884b050c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-metadata-api", + "description": "Magento module responsible for media gallery metadata implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadataApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml b/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml new file mode 100644 index 0000000000000..77adbc6efff88 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/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_MediaGalleryMetadataApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadataApi/registration.php b/app/code/Magento/MediaGalleryMetadataApi/registration.php new file mode 100644 index 0000000000000..90988681a5483 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryMetadataApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md new file mode 100644 index 0000000000000..df856e8003a84 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditions module + +The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json new file mode 100644 index 0000000000000..50b18752fc506 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions", + "description": "Magento module that implements height and width fields for for media gallery items.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditions\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..f23f94f186f68 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery_renditions" translate="label" type="text" sortOrder="1010" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Media Gallery Renditions</label> + <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Width</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Height</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml new file mode 100644 index 0000000000000..58c5aa1f11fd2 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery_renditions> + <width>1000</width> + <height>1000</height> + </media_gallery_renditions> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml new file mode 100644 index 0000000000000..792a9e128cc40 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryRenditions" /> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php new file mode 100644 index 0000000000000..275c06f752a63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditions', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php b/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php new file mode 100644 index 0000000000000..e558f23ab9608 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class responsible for providing access to Media Gallery Renditions system configuration. + */ +class Config +{ + /** + * Config path for Media Gallery Renditions Width + */ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + + /** + * Config path for Media Gallery Renditions Height + */ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get max width + * + * @return int + */ + public function getWidth(): int + { + return $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } + + /** + * Get max height + * + * @return int + */ + public function getHeight(): int + { + return $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md new file mode 100644 index 0000000000000..42478c0c9b520 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditionsApi module + +The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json new file mode 100644 index 0000000000000..6e3c559f001c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions-api", + "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditionsApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml new file mode 100644 index 0000000000000..64efa325ec791 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryRenditionsApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php new file mode 100644 index 0000000000000..bf057f2d2adbf --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditionsApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php new file mode 100644 index 0000000000000..b4b6713f47065 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Asset; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the asset options for multiselect filter + */ +class Search extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var SearchAssetsInterface + */ + private $searchAssets; + + /** + * @param SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Images + */ + private $images; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var Storage + */ + private $storage; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param FilterBuilder $filterBuilder + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param SearchAssetsInterface $searchAssets + * @param Context $context + * @param LoggerInterface $logger + * @param Images $images + * @param Storage $storage + */ + public function __construct( + FilterBuilder $filterBuilder, + SearchCriteriaBuilder $searchCriteriaBuilder, + FilterGroupBuilder $filterGroupBuilder, + SearchAssetsInterface $searchAssets, + Context $context, + LoggerInterface $logger, + Images $images, + Storage $storage + ) { + parent::__construct($context); + + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->logger = $logger; + $this->searchAssets = $searchAssets; + $this->images = $images; + $this->storage = $storage; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $searchKey = $this->getRequest()->getParam('searchKey'); + $limit = $this->getRequest()->getParam('limit'); + $pageNum = $this->getRequest()->getParam('page'); + $responseContent = []; + + if (!$searchKey) { + return $resultJson->setData([ + 'options' => [], + 'total' => 0 + ]); + } + + try { + $titleFilter = $this->filterBuilder->setField('title') + ->setConditionType('fulltext') + ->setValue($searchKey) + ->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$this->filterGroupBuilder->setFilters([$titleFilter])->create()]) + ->setPageSize($limit) + ->setCurrentPage($pageNum < 2 ? 0 : $pageNum) + ->create(); + + $assets = $this->searchAssets->execute($searchCriteria); + + if (!empty($assets)) { + foreach ($assets as $asset) { + $responseContent['options'][] = [ + 'value' => $asset->getId(), + 'label' => $asset->getTitle(), + 'path' => $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()) + ]; + $responseContent['total'] = count($responseContent['options']); + } + } + + $responseCode = self::HTTP_OK; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php new file mode 100644 index 0000000000000..3d4af88e4ad67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller to create the folders + */ +class Create extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var CreateDirectoriesByPathsInterface + */ + private $createDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param CreateDirectoriesByPathsInterface $createDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + CreateDirectoriesByPathsInterface $createDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->createDirectoriesByPaths = $createDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Create folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $paths = $this->getRequest()->getParam('paths'); + + if (!$paths) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder paths parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->createDirectoriesByPaths->execute($paths); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully created the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to create folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php new file mode 100644 index 0000000000000..56f12c5139d65 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the folders + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var DeleteDirectoriesByPathsInterface + */ + private $deleteDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->deleteDirectoriesByPaths = $deleteDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Delete folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $path = $this->getRequest()->getParam('path'); + + if (!$path) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder path parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->deleteDirectoriesByPaths->execute([$path]); + $this->deleteAssetsByPaths->execute([$path]); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully removed the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to remove folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php new file mode 100644 index 0000000000000..229a717ef13dd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\MediaGalleryUi\Model\Directories\FolderTree; +use Psr\Log\LoggerInterface; + +/** + * Returns all available directories + */ +class GetTree extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FolderTree + */ + private $folderTree; + + /** + * Constructor + * + * @param Action\Context $context + * @param LoggerInterface $logger + * @param FolderTree $folderTree + */ + public function __construct( + Action\Context $context, + LoggerInterface $logger, + FolderTree $folderTree + ) { + parent::__construct($context); + $this->logger = $logger; + $this->folderTree = $folderTree; + } + /** + * @inheritdoc + */ + public function execute() + { + try { + $responseContent[] = $this->folderTree->buildTree(); + $responseCode = self::HTTP_OK; + } catch (\Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('Retrieving directories list failed.'), + ]; + } + + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php new file mode 100644 index 0000000000000..a5d1cee7abf41 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryUi\Model\DeleteImage; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the media gallery content + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var DeleteImage + */ + private $deleteImage; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Delete constructor. + * + * @param Context $context + * @param DeleteImage $deleteImage + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param Storage $imagesStorage + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteImage $deleteImage, + GetAssetsByIdsInterface $getAssetsByIds, + Storage $imagesStorage, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteImage = $deleteImage; + $this->getAssetsByIds = $getAssetsByIds; + $this->imagesStorage = $imagesStorage; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $imageIds = $this->getRequest()->getParam('ids'); + + if (empty($imageIds) || !is_array($imageIds)) { + $responseContent = [ + 'success' => false, + 'message' => __('Image Ids are required and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $assets = $this->getAssetsByIds->execute($imageIds); + $this->deleteImage->execute($assets); + $responseCode = self::HTTP_OK; + if (count($imageIds) === 1) { + $message = __( + 'The asset "%title" has been successfully deleted.', + [ + 'title' => current($assets)->getTitle() + ] + ); + } else { + $message = __( + '%count assets have been successfully deleted.', + [ + 'count' => count($imageIds) + ] + ); + } + $responseContent = [ + 'success' => true, + 'message' => $message, + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to delete image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php new file mode 100644 index 0000000000000..d959a070148ed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\GetDetailsByAssetId; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the media gallery image details + */ +class Details extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var GetDetailsByAssetId + */ + private $getDetailsByAssetId; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Details constructor. + * + * @param Context $context + * @param GetDetailsByAssetId $getDetailsByAssetId + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + GetDetailsByAssetId $getDetailsByAssetId, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->logger = $logger; + $this->getDetailsByAssetId = $getDetailsByAssetId; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $ids = $this->getRequest()->getParam('ids'); + + if (empty($ids) || !is_array($ids)) { + $responseContent = [ + 'success' => false, + 'message' => __('Assets Ids is required, and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $details = $this->getDetailsByAssetId->execute($ids); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'imageDetails' => $details + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php new file mode 100644 index 0000000000000..f41c489607b15 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryUi\Model\UpdateAsset; +use Psr\Log\LoggerInterface; + +class SaveDetails extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var UpdateAsset + */ + private $updateAsset; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param MetadataInterfaceFactory $metadataFactory + * @param UpdateAsset $updateAsset + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + MetadataInterfaceFactory $metadataFactory, + UpdateAsset $updateAsset, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->metadataFactory = $metadataFactory; + $this->updateAsset = $updateAsset; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $assetId = (int) $this->getRequest()->getParam('id'); + $title = $this->getRequest()->getParam('title'); + $description = $this->getRequest()->getParam('description'); + $keywords = (array) $this->getRequest()->getParam('keywords'); + + if ($assetId === 0) { + $responseContent = [ + 'success' => false, + 'message' => __('Image ID is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->updateAsset->execute( + $assetId, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully saved the image "%image"', ['image' => $title]), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to save image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php new file mode 100644 index 0000000000000..e965d94b33f0c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\UploadImage; +use Psr\Log\LoggerInterface; + +/** + * Controller responsible to upload the media gallery content + */ +class Upload extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param UploadImage $upload + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + UploadImage $upload, + LoggerInterface $logger + ) { + parent::__construct($context); + $this->uploadImage = $upload; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $targetFolder = $this->getRequest()->getParam('target_folder'); + $type = $this->getRequest()->getParam('type'); + + if (!$targetFolder) { + $responseContent = [ + 'success' => false, + 'message' => __('The target_folder parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->uploadImage->execute($targetFolder, $type); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('The image was uploaded successfully.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => __('Could not upload image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php new file mode 100644 index 0000000000000..e97d93d86bb0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Index; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\View\Result\Layout; +use Magento\Framework\View\Result\LayoutFactory; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * Index constructor. + * + * @param Context $context + * @param LayoutFactory $layoutFactory + */ + public function __construct( + Context $context, + LayoutFactory $layoutFactory + ) { + parent::__construct($context); + $this->layoutFactory = $layoutFactory; + } + + /** + * Get the media gallery layout + * + * @return Layout + */ + public function execute(): Layout + { + return $this->layoutFactory->create(); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php new file mode 100644 index 0000000000000..3660374243d16 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Media; + +use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * Get the media gallery layout + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->setActiveMenu('Magento_MediaGalleryUi::media_gallery') + ->addBreadcrumb(__('Media'), __('Media Gallery')); + $resultPage->getConfig()->getTitle()->prepend(__('Manage Gallery')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryUi/LICENSE.txt b/app/code/Magento/MediaGalleryUi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php new file mode 100644 index 0000000000000..7c3eccfea521f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset created at date time + */ +class CreatedAt implements AssetDetailsProviderInterface +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @param TimezoneInterface $dateTime + */ + public function __construct( + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + } + + /** + * Provide asset created at date time + * + * @param AssetInterface $asset + * @return array + * @throws \Exception + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Created'), + 'value' => $this->formatDate($asset->getCreatedAt()) + ]; + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + * @throws \Exception + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php new file mode 100644 index 0000000000000..b2b0f389f6b9a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset height + */ +class Height implements AssetDetailsProviderInterface +{ + /** + * Provide asset height + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Height'), + 'value' => sprintf('%spx', $asset->getHeight()) + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php new file mode 100644 index 0000000000000..55841cc5abd3f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset file size + */ +class Size implements AssetDetailsProviderInterface +{ + /** + * Provide asset file size + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Size'), + 'value' => $this->formatImageSize($asset->getSize()) + ]; + } + + /** + * Format image size + * + * @param int $imageSize + * + * @return string + */ + private function formatImageSize(int $imageSize): string + { + if ($imageSize === 0) { + return ''; + } + + return sprintf('%sKb', $imageSize / 1000); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php new file mode 100644 index 0000000000000..5b47616398ef7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset type + */ +class Type implements AssetDetailsProviderInterface +{ + /** + * @var array + */ + private $types; + + /**= + * @param array $types + */ + public function __construct(array $types = []) + { + $this->types = $types; + } + + /** + * Provide asset type + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Type'), + 'value' => $this->getImageTypeByContentType($asset->getContentType()), + ]; + } + + /** + * Return image type by content type + * + * @param string $contentType + * @return string + */ + private function getImageTypeByContentType(string $contentType): string + { + $type = current(explode('/', $contentType)); + + return isset($this->types[$type]) ? $this->types[$type] : 'Asset'; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php new file mode 100644 index 0000000000000..2f50bd9a72208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset updated at date time + */ +class UpdatedAt implements AssetDetailsProviderInterface +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @param TimezoneInterface $dateTime + */ + public function __construct( + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + } + + /** + * Provide asset updated at date time + * + * @param AssetInterface $asset + * @return array + * @throws \Exception + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Modified'), + 'value' => $this->formatDate($asset->getUpdatedAt()) + ]; + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + * @throws \Exception + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php new file mode 100644 index 0000000000000..ca3883d5c937c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide information on which content asset is used in + */ +class UsedIn implements AssetDetailsProviderInterface +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContent; + + /** + * @var array + */ + private $contentTypes; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @param GetContentByAssetIdsInterface $getContent + * @param UrlInterface $url + * @param array $contentTypes + */ + public function __construct( + GetContentByAssetIdsInterface $getContent, + UrlInterface $url, + array $contentTypes = [] + ) { + $this->getContent = $getContent; + $this->url = $url; + $this->contentTypes = $contentTypes; + } + + /** + * Provide information on which content asset is used in + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Used In'), + 'value' => $this->getUsedIn($asset->getId()) + ]; + } + + /** + * Retrieve assets used in the Content + * + * @param int $assetId + * @return array + * @throws IntegrationException + */ + private function getUsedIn(int $assetId): array + { + $details = []; + + foreach ($this->getUsedInCounts($assetId) as $type => $number) { + $details[$type] = $this->contentTypes[$type] ?? ['name' => $type, 'link' => null]; + $details[$type]['number'] = $number; + $details[$type]['link'] = $details[$type]['link'] ? $this->url->getUrl($details[$type]['link']) : null; + } + + return array_values($details); + } + + /** + * Get used in counts per type + * + * @param int $assetId + * @return int[] + * @throws IntegrationException + */ + private function getUsedInCounts(int $assetId): array + { + $usedIn = []; + $entityIds = []; + + $contentIdentities = $this->getContent->execute([$assetId]); + + foreach ($contentIdentities as $contentIdentity) { + $entityId = $contentIdentity->getEntityId(); + $type = $contentIdentity->getEntityType(); + + if (!isset($entityIds[$type])) { + $usedIn[$type] = 1; + } elseif ($entityIds[$type]['entity_id'] !== $entityId) { + ++$usedIn[$type]; + } + $entityIds[$type]['entity_id'] = $entityId; + } + return $usedIn; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php new file mode 100644 index 0000000000000..64e9cf8ad1a8f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset width + */ +class Width implements AssetDetailsProviderInterface +{ + /** + * Provide asset width + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Width'), + 'value' => sprintf('%spx', $asset->getWidth()) + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php new file mode 100644 index 0000000000000..92375adfdd4f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +interface AssetDetailsProviderInterface +{ + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array; +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php new file mode 100644 index 0000000000000..207f35bb99d6a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +class AssetDetailsProviderPool +{ + /** + * @var AssetDetailsProviderInterface[] + */ + private $detailsProviders; + + /** + * @param AssetDetailsProviderInterface[] $detailsProviders + */ + public function __construct(array $detailsProviders = []) + { + $this->detailsProviders = $detailsProviders; + } + + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array + { + $details = []; + foreach ($this->detailsProviders as $detailsProvider) { + if ($detailsProvider instanceof AssetDetailsProviderInterface) { + $details[] = $detailsProvider->execute($asset); + } + } + return $details; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Config.php b/app/code/Magento/MediaGalleryUi/Model/Config.php new file mode 100644 index 0000000000000..a9391d76428ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Config.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Class responsible to provide access to system configuration related to the Media Gallery + */ +class Config implements ConfigInterface +{ + /** + * Path to enable/disable media gallery in the system settings. + */ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if masonry grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php new file mode 100644 index 0000000000000..2f4793c28ad47 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Delete image from a storage + */ +class DeleteImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * DeleteImage constructor. + * + * @param Storage $imagesStorage + * @param Filesystem $filesystem + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem, + IsPathExcludedInterface $isPathExcluded + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Delete asset image physically from file storage and from data storage. + * + * @param AssetInterface[] $assets + * @throws LocalizedException + */ + public function execute(array $assets): void + { + $failedAssets = []; + foreach ($assets as $asset) { + if ($this->isPathExcluded->execute($asset->getPath())) { + $failedAssets[] = $asset->getPath(); + } + + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $absolutePath = $mediaDirectory->getAbsolutePath($asset->getPath()); + $this->imagesStorage->deleteFile($absolutePath); + } + if (!empty($failedAssets)) { + throw new LocalizedException( + __( + 'Could not delete "%image": destination directory is restricted.', + ['image' => implode(",", $failedAssets)] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php new file mode 100644 index 0000000000000..574b8aab8bcd3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Directories; + +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Build folder tree structure by path + */ +class FolderTree +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $path; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * Constructor + * + * @param Filesystem $filesystem + * @param string $path + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Filesystem $filesystem, + string $path, + IsPathExcludedInterface $isPathExcluded + ) { + $this->filesystem = $filesystem; + $this->path = $path; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Return directory folder structure in array + * + * @param bool $skipRoot + * @return array + * @throws ValidatorException + */ + public function buildTree(bool $skipRoot = true): array + { + return $this->buildFolderTree($this->getDirectories(), $skipRoot); + } + + /** + * Build directory tree array in format for jstree strandart + * + * @return array + * @throws ValidatorException + */ + private function getDirectories(): array + { + $directories = []; + + /** @var Read $directory */ + $directory = $this->filesystem->getDirectoryRead($this->path); + + if (!$directory->isDirectory()) { + return $directories; + } + + foreach ($directory->readRecursively() as $path) { + if (!$directory->isDirectory($path) || $this->isPathExcluded->execute($path)) { + continue; + } + + $pathArray = explode('/', $path); + $directories[] = [ + 'data' => count($pathArray) > 0 ? end($pathArray) : $path, + 'attr' => ['id' => $path], + 'metadata' => [ + 'path' => $path + ], + 'path_array' => $pathArray + ]; + } + return $directories; + } + + /** + * Build folder tree structure by provided directories path + * + * @param array $directories + * @param bool $skipRoot + * @return array + */ + private function buildFolderTree(array $directories, bool $skipRoot): array + { + $tree = [ + 'name' => 'root', + 'path' => '/', + 'children' => [] + ]; + foreach ($directories as $idx => &$node) { + $node['children'] = []; + $result = $this->findParent($node, $tree); + $parent = & $result['treeNode']; + + $parent['children'][] =& $directories[$idx]; + } + return $skipRoot ? $tree['children'] : $tree; + } + + /** + * Find parent directory + * + * @param array $node + * @param array $treeNode + * @param int $level + * @return array + */ + private function findParent(array &$node, array &$treeNode, int $level = 0): array + { + $nodePathLength = count($node['path_array']); + $treeNodeParentLevel = $nodePathLength - 1; + + $result = ['treeNode' => &$treeNode]; + + if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) { + return $result; + } + + foreach ($treeNode['children'] as &$tnode) { + if ($node['path_array'][$level] === $tnode['path_array'][$level]) { + return $this->findParent($node, $tnode, $level + 1); + } + } + return $result; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php new file mode 100644 index 0000000000000..b870082ea2aa1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Exception; +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Load Media Asset from database by id add all related data to it + */ +class GetDetailsByAssetId +{ + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsById; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var SourceIconProvider + */ + private $sourceIconProvider; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @var AssetDetailsProviderPool + */ + private $detailsProviderPool; + + /** + * @param AssetDetailsProviderPool $detailsProviderPool + * @param GetAssetsByIdsInterface $getAssetById + * @param StoreManagerInterface $storeManager + * @param SourceIconProvider $sourceIconProvider + * @param GetAssetsKeywordsInterface $getAssetKeywords + */ + public function __construct( + AssetDetailsProviderPool $detailsProviderPool, + GetAssetsByIdsInterface $getAssetById, + StoreManagerInterface $storeManager, + SourceIconProvider $sourceIconProvider, + GetAssetsKeywordsInterface $getAssetKeywords + ) { + $this->detailsProviderPool = $detailsProviderPool; + $this->getAssetsById = $getAssetById; + $this->storeManager = $storeManager; + $this->sourceIconProvider = $sourceIconProvider; + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * Get image details by assets Ids + * + * @param array $assetIds + * @throws LocalizedException + * @throws Exception + * @return array + */ + public function execute(array $assetIds): array + { + $assets = $this->getAssetsById->execute($assetIds); + + $details = []; + foreach ($assets as $asset) { + $details[$asset->getId()] = [ + 'image_url' => $this->getUrl($asset->getPath()), + 'title' => $asset->getTitle(), + 'path' => $asset->getPath(), + 'description' => $asset->getDescription(), + 'id' => $asset->getId(), + 'details' => $this->detailsProviderPool->execute($asset), + 'size' => $asset->getSize(), + 'tags' => $this->getKeywords($asset), + 'source' => $asset->getSource() ? + $this->sourceIconProvider->getSourceIconUrl($asset->getSource()) : + null, + 'content_type' => strtoupper(str_replace('image/', '', $asset->getContentType())), + ]; + } + return $details; + } + + /** + * Key asset keywords + * + * @param AssetInterface $asset + * @return string[] + */ + private function getKeywords(AssetInterface $asset): array + { + $assetKeywords = $this->getAssetKeywords->execute([$asset->getId()]); + + if (empty($assetKeywords)) { + return []; + } + + $keywords = current($assetKeywords)->getKeywords(); + + return array_map( + function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, + $keywords + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * + * @return string + * + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $path; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..88401465d56b7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Listing; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider as UiComponentDataProvider; +use Magento\MediaGalleryUi\Ui\Component\Listing\Provider; + +/** + * Media gallery UI data provider. Try catch added for displaying errors in grid + */ +class DataProvider extends UiComponentDataProvider +{ + /** + * @var CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory $collectionFactory + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $collectionFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->collectionFactory = $collectionFactory; + $this->collectionProcessor = $collectionProcessor; + } + + /** + * @inheritdoc + */ + public function getData(): array + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + /** @var Provider $collection */ + $collection = $this->collectionFactory->getReport($this->getSearchCriteria()->getRequestName()); + $this->collectionProcessor->process($this->getSearchCriteria(), $collection); + + return $collection; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php new file mode 100644 index 0000000000000..785c3078cdbe5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to filter a content field + */ +class ContentField implements CustomFilterInterface +{ + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentStatus; + + /** + * ContentField constructor. + * + * @param GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + */ + public function __construct( + GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + ) { + $this->getAssetIdsByContentStatus = $getAssetIdsByContentStatus; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $collection->addFieldToFilter( + 'main_table.id', + ['in' => $this->getAssetIdsByContentStatus->execute($filter->getField(), $filter->getValue())] + ); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php new file mode 100644 index 0000000000000..36e9375525f8d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; + +class Directory implements CustomFilterInterface +{ + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = str_replace('%', '', $filter->getValue()); + $collection->getSelect()->where('path REGEXP ? ', '^' . $value . '/[^\/]*$'); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php new file mode 100644 index 0000000000000..d43b3ac2ca451 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +/** + * Custom filter to filter collection by duplicated hash values + */ +class Duplicated implements CustomFilterInterface +{ + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + if ($filter->getValue()) { + $collection->getSelect()->where('main_table.hash IN (?)', $this->getDuplicatedIds()); + } + return true; + } + /** + * Return sql part of duplicated values. + */ + private function getDuplicatedIds(): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select() + ->from($this->connection->getTableName('media_gallery_asset'), ['hash']) + ->group('hash') + ->having('COUNT(*) > 1') + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php new file mode 100644 index 0000000000000..1d8aa78a03cc2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +/** + * Custom filter to filter collection by entity type + */ +class Entity implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var string + */ + private $entityType; + + /** + * @param ResourceConnection $resource + * @param string $entityType + */ + public function __construct(ResourceConnection $resource, string $entityType) + { + $this->connection = $resource; + $this->entityType = $entityType; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $ids = $filter->getValue(); + if (is_array($ids)) { + $collection->addFieldToFilter( + self::TABLE_ALIAS . '.id', + ['in' => $this->getSelectByEntityIds($ids)] + ); + } + return true; + } + + /** + * Return select asset ids by entity type + * + * @param array $ids + * @return Select + */ + private function getSelectByEntityIds(array $ids): Select + { + return $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->where( + 'entity_id IN (?)', + $ids + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php new file mode 100644 index 0000000000000..1b5e2282ff3dc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +/** + * Custom filter to filter collection by entity type + */ +class EntityType implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const NOT_USED = 'not_used'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + if (is_array($value)) { + $conditions = []; + + if (in_array(self::NOT_USED, $value)) { + unset($value[array_search(self::NOT_USED, $value)]); + $conditions[] = ['in' => $this->getNotUsedEntityIds()]; + } + + if (!empty($value)) { + $conditions[] = ['in' => $this->getEntityTypesIds($value)]; + } + + $collection->addFieldToFilter( + self::TABLE_ALIAS . '.id', + $conditions + ); + } + return true; + } + + /** + * Return asset ids by entity type + * + * @param array $value + * @return array + */ + private function getEntityTypesIds(array $value): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type IN (?)', + $value + ) + ); + } + + /** + * Return asset ids that not exists in asset_content_table + */ + private function getNotUsedEntityIds(): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + ['media_gallery_asset' => $this->connection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET)], + ['id'] + )->where( + 'media_gallery_asset.id not in ?', + $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + ) + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php new file mode 100644 index 0000000000000..a3003e3f5a23a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +class Keyword implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_KEYWORDS = 'media_gallery_asset_keyword'; + private const TABLE_ASSET_KEYWORD = 'media_gallery_keyword'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + + $collection->addFieldToFilter( + [self::TABLE_ALIAS . '.title', self::TABLE_ALIAS . '.id'], + [ + ['like' => sprintf('%%%s%%', $value)], + ['in' => $this->getAssetIdsByKeyword($value)] + ] + ); + + return true; + } + + /** + * Return asset ids by keyword + * + * @param string $value + * @return array + */ + private function getAssetIdsByKeyword(string $value): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select()->from( + $connection->select() + ->from( + ['asset_keywords_table' => $this->connection->getTableName(self::TABLE_ASSET_KEYWORD)], + ['id'] + )->where( + 'keyword = ?', + $value + )->joinInner( + ['keywords_table' => $this->connection->getTableName(self::TABLE_KEYWORDS)], + 'keywords_table.keyword_id = asset_keywords_table.id', + ['asset_id'] + ), + ['asset_id'] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php new file mode 100644 index 0000000000000..ff82b990d2a01 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryUi\Model\UpdateAsset\UpdateKeywords; +use Magento\MediaGalleryUi\Model\UpdateAsset\SaveMetadataToFile; + +class UpdateAsset +{ + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var SaveMetadataToFile + */ + private $processMetadata; + + /** + * @var UpdateKeywords + */ + private $processKeywords; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param SaveAssetsInterface $saveAssets + * @param UpdateKeywords $processKeywords + * @param SaveMetadataToFile $processMetadata + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByIdsInterface $getAssetsByIds, + SaveAssetsInterface $saveAssets, + UpdateKeywords $processKeywords, + SaveMetadataToFile $processMetadata + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByIds = $getAssetsByIds; + $this->saveAssets = $saveAssets; + $this->processKeywords = $processKeywords; + $this->processMetadata = $processMetadata; + } + + /** + * Save asset details + * + * @param int $id + * @param MetadataInterface $data + */ + public function execute(int $id, MetadataInterface $data): void + { + $asset = $this->getAsset($id); + + $updatedAsset = $this->assetFactory->create( + [ + 'path' => $asset->getPath(), + 'contentType' => $asset->getContentType(), + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + 'size' => $asset->getSize(), + 'id' => $asset->getId(), + 'title' => $data->getTitle() ?? $asset->getTitle(), + 'description' => $data->getDescription() ?? $asset->getDescription(), + 'source' => $asset->getSource(), + 'hash' => $asset->getHash(), + 'created_at' => $asset->getCreatedAt(), + 'updated_at' => $asset->getUpdatedAt() + ] + ); + + $this->saveAssets->execute([$updatedAsset]); + $this->processMetadata->execute($asset->getPath(), $data); + + $keywords = $data->getKeywords(); + if (isset($keywords)) { + $this->processKeywords->execute($id, $keywords); + } + } + + /** + * Load asset by id + * + * @param int $id + * @return AssetInterface + * @throws LocalizedException + */ + private function getAsset(int $id): AssetInterface + { + $assets = $this->getAssetsByIds->execute([$id]); + if (empty($assets)) { + throw new LocalizedException(__('Could not retrieve the asset.')); + } + return current($assets); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php new file mode 100644 index 0000000000000..3ebe04374f81e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Psr\Log\LoggerInterface; + +class SaveMetadataToFile +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param AddMetadataInterface $addMetadata + * @param LoggerInterface $logger + */ + public function __construct( + Filesystem $filesystem, + AddMetadataInterface $addMetadata, + LoggerInterface $logger + ) { + $this->filesystem = $filesystem; + $this->addMetadata = $addMetadata; + $this->logger = $logger; + } + + /** + * Save updated metadata + * + * @param string $path + * @param MetadataInterface $data + */ + public function execute(string $path, MetadataInterface $data): void + { + $absolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path); + + try { + $this->addMetadata->execute($absolutePath, $data); + } catch (LocalizedException $e) { + $this->logger->critical($e); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php new file mode 100644 index 0000000000000..2a359d5a14025 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; + +class UpdateKeywords +{ + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param KeywordInterfaceFactory $keywordFactory + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + */ + public function __construct( + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + KeywordInterfaceFactory $keywordFactory, + SaveAssetsKeywordsInterface $saveAssetKeywords + ) { + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->keywordFactory = $keywordFactory; + $this->saveAssetKeywords = $saveAssetKeywords; + } + + /** + * Save asset keywords + * + * @param int $assetId + * @param string[] $keywords + */ + public function execute(int $assetId, array $keywords): void + { + $this->saveAssetKeywords->execute([ + $this->assetKeywordsFactory->create([ + 'assetId' => $assetId, + 'keywords' => $this->createKeywords($keywords) + ]) + ]); + } + + /** + * Create keyword objects from strings + * + * @param string[] $keywords + * @return KeywordInterface[] + */ + private function createKeywords(array $keywords): array + { + $keywordObjects = []; + foreach ($keywords as $keyword) { + $keywordObjects[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + return $keywordObjects; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UploadImage.php b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php new file mode 100644 index 0000000000000..c918548bea553 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; + +/** + * Uploads an image to storage + */ +class UploadImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Storage $imagesStorage + * @param Filesystem $filesystem + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + } + + /** + * Uploads the image and returns file object + * + * @param string $targetFolder + * @param string $type + * @throws LocalizedException + */ + public function execute(string $targetFolder, string $type): void + { + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + if (!$mediaDirectory->isDirectory($targetFolder)) { + throw new LocalizedException(__('Directory %1 does not exist in media directory.', $targetFolder)); + } + + $this->imagesStorage->uploadFile($mediaDirectory->getAbsolutePath($targetFolder), $type); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php new file mode 100644 index 0000000000000..7988ac2d9e635 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Plugin; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite; + +/** + * Create resizes files that were synced + */ +class CreateThumbnails +{ + /** + * @var Storage + */ + private $storage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + * @param Storage $storage + */ + public function __construct(Filesystem $filesystem, Storage $storage) + { + $this->storage = $storage; + $this->filesystem = $filesystem; + } + + /** + * Create thumbnails for synced files. + * + * @param ImportFilesComposite $subject + * @param string[] $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(ImportFilesComposite $subject, array $paths): array + { + foreach ($paths as $path) { + $this->storage->resizeFile( + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path) + ); + } + + return [$paths]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md new file mode 100644 index 0000000000000..6fbad656b23a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUi module + +The Magento_MediaGalleryUi module is responsible for the media gallery user interface (UI) implementation. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..c056727aa8fe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.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="AdminAssertImageInStandaloneMediaGalleryActionGroup"> + <annotations> + <description>Validates that the provided image is present and correct in the standalone media gallery.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + + <seeElement selector="{{AdminEnhancedMediaGalleryActionsSection.imageSrc(imageName)}}" + stepKey="checkFirstImageAfterSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..d47eb491f9b5d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.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="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup"> + <annotations> + <description>Adds image to target element from View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.addImage}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml new file mode 100644 index 0000000000000..9a550805a7dec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml @@ -0,0 +1,18 @@ +<?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="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup"> + <annotations> + <description>Applies duplicated images filter to the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.duplicatedFilterCheckbox}}" stepKey="clickShowDuplicates"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..9d7d725cf49de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.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="AdminEnhancedMediaGalleryApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.applyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml new file mode 100644 index 0000000000000..aeee921f92e58 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.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="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup"> + <annotations> + <description>Assert media gallery grid filters</description> + </annotations> + <arguments> + <argument name="resultValue" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFiltersToCheckAppliedFilter"/> + <see selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilter(resultValue)}}" userInput="{{resultValue}}" stepKey="verifyAppliedFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml new file mode 100644 index 0000000000000..7f4db971702ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml @@ -0,0 +1,18 @@ +<?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="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup"> + <annotations> + <description>Asserts images has been deleted in mass action.</description> + </annotations> + + <see userInput='Assets have been successfully deleted' stepKey="verifyDeleteImages"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml new file mode 100644 index 0000000000000..efcf40cd2b644 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.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="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup"> + <annotations> + <description>Asserts that massaction mode can be enabled and disabled, verify massaction view after switch to massaction mode</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + <see selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" userInput="(1 Selected)" stepKey="verifySelectedCount"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml new file mode 100644 index 0000000000000..a691f65387e8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.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="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup"> + <annotations> + <description>Asserts that massaction mode is terminated</description> + </annotations> + + + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMassAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml new file mode 100644 index 0000000000000..783e71719c659 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.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="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup"> + <annotations> + <description>Assert that grid have no active filter</description> + </annotations> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilterPlaceholder}}" stepKey="assertThereIsNoActiveFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml new file mode 100644 index 0000000000000..b53e76e06cfb5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.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="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup"> + <annotations> + <description>Assert image delete action popup contains warnin message</description> + </annotations> + <arguments> + <argument name="messageText" type="string"/> + </arguments> + + <see userInput="{{messageText}}" stepKey="assertWarningMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..478ca2b3b5be9 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.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="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in Category grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridApplyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..00608504fd7a6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.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="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery category filters by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridFiltersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml new file mode 100644 index 0000000000000..600e1cd747943 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.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="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup"> + <annotations> + <description>Click delete images button.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="clickDeleteImages"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml new file mode 100644 index 0000000000000..3754eb319da44 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.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="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup"> + <annotations> + <description>Closes View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.cancel}}" stepKey="clickCancel"/> + <wait time="1" stepKey="waitForElementRender"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml new file mode 100644 index 0000000000000..90546eca8dc0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.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="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup"> + <annotations> + <description>Click confirm on confirmation popup images delete action.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeletingProcces"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml new file mode 100644 index 0000000000000..d3d1f0aaf39de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryDeleteGridViewActionGroup"> + <annotations> + <description>Delete grid view bookmarks by name</description> + </annotations> + <arguments> + <argument name="viewToDelete" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(viewToDelete)}}{{AdminAdobeStockSection.editViewButtonPartial}}" stepKey="clickEditButton"/> + <seeElement selector="{{AdminAdobeStockSection.deleteViewButton}}" stepKey="seeDeleteButton"/> + <click selector="{{AdminAdobeStockSection.deleteViewButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForDeletion" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml new file mode 100644 index 0000000000000..f404ffbe7c4f0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.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="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup"> + <annotations> + <description>Disable massaction mode by clicking on cancel button</description> + </annotations> + + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.cancelMassActionMode}}" stepKey="cancelMassAction"/> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMAssAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..84712e8e3f3ae --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.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="AdminEnhancedMediaGalleryEditImageDetailsActionGroup"> + <annotations> + <description>Opens Edit image details panel panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.edit}}" stepKey="edit"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml new file mode 100644 index 0000000000000..4b0375088509b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.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="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup"> + <annotations> + <description>Activate massaction mode by click on Delete Selected..</description> + </annotations> + + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="waitForMassActionButton"/> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="clickOnMassActionButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..d2ac1c78b2582 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.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="AdminEnhancedMediaGalleryExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery filter by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..b3733ceb4c4a0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Media Gallery</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.delete}}" stepKey="deleteImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml new file mode 100644 index 0000000000000..001aa010dbdd4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.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="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup"> + <annotations> + <description>Delete image from the View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.delete}}" stepKey="deleteImage"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="waitForConfirmation"/> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml new file mode 100644 index 0000000000000..931da0ee06fef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml @@ -0,0 +1,18 @@ +<?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="AdminEnhancedMediaGalleryImageDetailsEditActionGroup"> + <annotations> + <description>Edit image from the View Details panel</description> + </annotations> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> + <waitForLoadingMaskToDisappear 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 new file mode 100644 index 0000000000000..0da3de9501c13 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup"> + <annotations> + <description>Save image details from the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <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"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml new file mode 100644 index 0000000000000..57096124c0370 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminEnhancedMediaGallerySaveCustomViewActionGroup"> + <annotations> + <description>Save custom view media gallery</description> + </annotations> + <arguments> + <argument name="viewName" type="string" defaultValue="Test View"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.saveViewAs}}" stepKey="saveView"/> + <fillField selector="{{AdminGridDefaultViewControls.viewName}}" userInput="{{viewName}}" stepKey="inputViewName"/> + <pressKey selector="{{AdminGridDefaultViewControls.viewName}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml new file mode 100644 index 0000000000000..4244724599fed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.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="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup"> + <annotations> + <description>Apply custom bookmarks view to the media gallery grid</description> + </annotations> + <arguments> + <argument name="selectView" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(selectView)}}" stepKey="clickOnViewButton"/> + <waitForPageLoad stepKey="waitForGridLoad" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml new file mode 100644 index 0000000000000..6532fb869d2cc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.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="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup"> + <annotations> + <description>Select images in grid by clicking on mass action checkbox</description> + </annotations> + <arguments> + <argument name="imageName" type="string" defaultValue="magento"/> + </arguments> + + <checkOption selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml new file mode 100644 index 0000000000000..9be288b064742 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectSourceFilterActionGroup"> + <annotations> + <description>Select source filter by provided option</description> + </annotations> + <arguments> + <argument type="string" name="filterValue"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.sourceFilterValue(filterValue)}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..72d01e1871513 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup"> + <annotations> + <description>Set search options filter</description> + </annotations> + <arguments> + <argument type="string" name="filterName"/> + <argument type="string" name="optionName"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilter(filterName)}}" stepKey="openFilter"/> + <fillField selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterInput(filterName)}}" userInput="{{optionName}}" stepKey="enterOptionName" /> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterOption(filterName, optionName)}}" stepKey="selectOption"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone(filterName)}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml new file mode 100644 index 0000000000000..053a1185b3fda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminEnhancedMediaGalleryUploadImageActionGroup"> + <annotations> + <description>Uploads the provided Image to Media Gallery. + If you use this action group, you MUST add steps to delete the image in the "after" steps.</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <attachFile selector="{{AdminEnhancedMediaGalleryActionsSection.upload}}" userInput="{{image.value}}" stepKey="uploadImage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml new file mode 100644 index 0000000000000..eb2fc79567d08 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup"> + <annotations> + <description>Verifies image description on the View Details panel</description> + </annotations> + <arguments> + <argument name="description"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.description}}" stepKey="grabDescription"/> + <assertStringContainsString stepKey="verifyDescription"> + <actualResult type="variable">grabDescription</actualResult> + <expectedResult type="string">{{description}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..1ebaa0581e33e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml @@ -0,0 +1,37 @@ +<?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="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup"> + <annotations> + <description>Verifies image information on the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.type}}" stepKey="grabType"/> + <assertStringContainsString stepKey="verifyType"> + <actualResult type="variable">grabType</actualResult> + <expectedResult type="string">Image</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml new file mode 100644 index 0000000000000..4c38b7dbc8c3e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup"> + <annotations> + <description>Verifies image filename on the View Details panel</description> + </annotations> + <arguments> + <argument name="filename" type="string"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.filename}}" stepKey="grabFilename"/> + <assertStringContainsString stepKey="verifyFilename"> + <actualResult type="variable">grabFilename</actualResult> + <expectedResult type="string">{{filename}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml new file mode 100644 index 0000000000000..2fc4f7ea25fd0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup"> + <annotations> + <description>Verifies image keywords on the View Details panel</description> + </annotations> + <arguments> + <argument name="keywords"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> + <assertStringContainsString stepKey="verifyKeywords"> + <actualResult type="variable">grabKeywords</actualResult> + <expectedResult type="string">{{keywords}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml new file mode 100644 index 0000000000000..08dac976332ee --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup"> + <annotations> + <description>Verifies image title on the View Details panel</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..b5c0bbac69bec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.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="AdminEnhancedMediaGalleryViewImageDetails"> + <annotations> + <description>Opens View Details panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.viewDetails}}" stepKey="viewDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml new file mode 100644 index 0000000000000..6ddb6311c1a7e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.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="AdminMediaGalleryApplySelectFilterActionGroup"> + <annotations> + <description>Applies select filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="filterLabel" type="string"/> + <argument name="optionLabel" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilter(filterLabel)}}" stepKey="openSelectFilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilterOption(filterLabel, optionLabel)}}" stepKey="selectFilterOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..a930f65b71040 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.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="AdminMediaGalleryApplyUsedInFilterActionGroup"> + <annotations> + <description>Applies Show Images Used In filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="entityType" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInSelectDropdown}}" stepKey="openUsedInfilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInEntityType(entityType)}}" stepKey="selectEntityType"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone('Show Images Used In')}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml new file mode 100644 index 0000000000000..42d723f0811d3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.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="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup"> + <annotations> + <description>Asserts category name in category grid page</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', categoryName)}}" stepKey="assertNameColumn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml new file mode 100644 index 0000000000000..d0d9817da6d34 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml @@ -0,0 +1,18 @@ +<?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="AdminMediaGalleryAssertFolderDoesNotExistActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="5" stepKey="waitForFolderTreeReloads"/> + <dontSeeElement selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="folderDoesNotExist"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml new file mode 100644 index 0000000000000..7d71c764bc8de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.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="AdminMediaGalleryAssertFolderNameActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <waitForElementVisible selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="waitForFolder"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml new file mode 100644 index 0000000000000..6785558c8ef54 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.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="AdminMediaGalleryAssertImageInGridActionGroup"> + <annotations> + <description>Asserts that image exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml new file mode 100644 index 0000000000000..cc4de51357de0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.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="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup"> + <annotations> + <description>Asserts that image does not exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml new file mode 100644 index 0000000000000..28dcc1c553a5a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryClickAddSelectedActionGroup"> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="waitForAddSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="ClickAddSelected"/> + <wait time="5" stepKey="waitForImageToBeAdded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml new file mode 100644 index 0000000000000..ee2ff887488a4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.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="AdminMediaGalleryClickImageInGridActionGroup"> + <annotations> + <description>Select image on enhanced media gallery</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="waitForImageToBeVisible"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="clickOnImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml new file mode 100644 index 0000000000000..3e555c25e0a98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.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="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup"> + <annotations> + <description>Click ok button on upload image tinyMce4 popup.</description> + </annotations> + + <waitForElementVisible selector="{{MediaGallerySection.OkBtn}}" stepKey="waitForOkBtn"/> + <click selector="{{MediaGallerySection.OkBtn}}" stepKey="clickOkBtn"/> + <waitForPageLoad stepKey="wait"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml new file mode 100644 index 0000000000000..f3ccc8ef7be04 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml @@ -0,0 +1,18 @@ +<?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="AdminMediaGalleryCreateNewFolderActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <fillField selector="{{AdminMediaGalleryFolderSection.folderNameField}}" userInput="{{name}}" stepKey="setFolderName" /> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmCreateButton}}" stepKey="clickCreateButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml new file mode 100644 index 0000000000000..964b33dd38d55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryEditAssetAddKeywordActionGroup"> + <annotations> + <description>Set Keywords on the Edit Details panel</description> + </annotations> + <arguments> + <argument name="keyword"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.newKeyword}}" userInput="{{keyword}}" stepKey="enterKeyword"/> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.addNewKeyword}}" stepKey="addKeyword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml new file mode 100644 index 0000000000000..8791c1b152249 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminMediaGalleryEnhancedEnableActionGroup"> + <arguments> + <argument name="enabled" type="string" defaultValue="{{MediaGalleryConfigDataDisabled.value}}"/> + </arguments> + <amOnPage url="{{AdminConfigSystemPage.url}}" stepKey="navigateToSystemConfigurationPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <scrollTo selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="scrollToEnhancedMediaGalleryFieldset"/> + <conditionalClick stepKey="expandEnhancedMediaGalleryTab" selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" dependentSelector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" visible="false" /> + <waitForElementVisible selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="waitForFieldset" /> + <selectOption userInput="{{enabled}}" selector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" stepKey="enableOrDisableMediaGallery"/> + <click selector="{{AdminConfigSystemSection.saveConfig}}" stepKey="saveConfiguration"/> + <waitForPageLoad stepKey="waitForConfigurationToSave"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml new file mode 100644 index 0000000000000..f7e8f551e681f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.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="AdminMediaGalleryFolderDeleteActionGroup"> + <wait time="2" stepKey="waitBeforeDeleteButtonWillBeActive"/> + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderDeleteModalHeader}}" stepKey="waitBeforeModalAppears"/> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmDeleteButton}}" stepKey="clickConfirmDeleteButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml new file mode 100644 index 0000000000000..b8ed1d4f1cd25 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.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="AdminMediaGalleryFolderSelectActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="selectFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..e6cbbfbc1f48d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.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="AdminMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Enhanced Media Gallery using header delete button</description> + </annotations> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="waitForDeleteSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="ClickDeleteSelectedButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml new file mode 100644 index 0000000000000..165522892f271 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml @@ -0,0 +1,15 @@ +<?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="AdminMediaGalleryOpenNewFolderFormActionGroup"> + <click selector="{{AdminMediaGalleryFolderSection.folderNewCreateButton}}" stepKey="clickCreateNewFolderButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderNewModalHeader}}" stepKey="waitForModalOpen"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml new file mode 100644 index 0000000000000..6f38bd7c7d738 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.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="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from image uploader on category page</description> + </annotations> + + <conditionalClick stepKey="clickExpandContent" selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.selectFromGalleryButton}}" visible="false" /> + <waitForElementVisible selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="waitForSelectFromGallery" /> + <click selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="clickSelectFromGallery" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml new file mode 100644 index 0000000000000..0b2540de5288e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.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="AdminOpenMediaGalleryFromPageNoEditorActionGroup"> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="waitForInsertImageButton" /> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImage" /> + <!-- wait for initial media gallery load, where the gallery chrome loads (and triggers loading modal) --> + <waitForPageLoad stepKey="waitForMediaGalleryInitialLoad"/> + <!-- wait for second media gallery load, where the gallery images load (and triggers loading modal once more) --> + <waitForPageLoad stepKey="waitForMediaGallerySecondaryLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml new file mode 100644 index 0000000000000..3143b4ff24fb4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryTinyMce4ActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from category page by tyniMce4 image icon</description> + </annotations> + + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowse"/> + <waitForPageLoad stepKey="waitForPopup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..1ef908f34918e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml @@ -0,0 +1,15 @@ +<?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="AdminOpenStandaloneMediaGalleryActionGroup"> + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="amOnStandaloneMediaGalleryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml new file mode 100644 index 0000000000000..e9558ac87df3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.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="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup"> + <annotations> + <description>Assert that an image was deleted from Enhanced Media Gallery.</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <see userInput='The asset "{{title}}" has been successfully deleted' stepKey="verifyDeleteImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml new file mode 100644 index 0000000000000..ff11f1a5c7058 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AssertImageAddedToPageContentActionGroup"> + <annotations> + <description>Validates that the an image was added to the content.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <grabValueFrom selector="{{CmsNewPagePageContentSection.content}}" stepKey="grabTextFromContent"/> + <assertStringContainsString stepKey="assertContentContainsAddedImage"> + <expectedResult type="string">{{imageName}}</expectedResult> + <actualResult type="variable">grabTextFromContent</actualResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..e17be216335fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml @@ -0,0 +1,36 @@ +<?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="AssertImageAttributesOnEnhancedMediaGalleryActionGroup"> + <annotations> + <description>Assets image information on the Media Gallery grid</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.dimensions}}" stepKey="grabDimensions"/> + <assertNotEmpty stepKey="verifyDimensions"> + <actualResult type="variable">grabDimensions</actualResult> + </assertNotEmpty> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml new file mode 100644 index 0000000000000..1d568fb6a1da4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.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="SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup" extends="SearchAdminDataGridByKeywordActionGroup"> + <annotations> + <description>EXTENDS: SearchAdminDataGridByKeywordActionGroup. Fills 'Search by keyword' on an Standalone Media Gallery Admin Grid page. Clicks on Submit Search.</description> + </annotations> + <arguments> + <argument name="keyword" type="string" defaultValue=""/> + </arguments> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml new file mode 100644 index 0000000000000..1ec5e7d802a61 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.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="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup"> + <arguments> + <argument name="categoryEntity" defaultValue="SimpleSubCategory"/> + <argument name="imageName" type="string"/> + </arguments> + <annotations> + <description>Navigates to the category page on the storefront and asserts that the image is present in description.</description> + </annotations> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="openHomePage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryEntity.name)}}" stepKey="toCategory"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.imageSource(imageName)}}" stepKey="seeImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml new file mode 100644 index 0000000000000..dbc298798ee8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml @@ -0,0 +1,43 @@ +<?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="UpdatedImageDetails" type="image"> + <data key="title">renamed title</data> + <data key="description">test description</data> + <data key="file">magento.jpg</data> + <data key="fileName">renamed title</data> + <data key="extension">jpg</data> + <data key="keyword">newkeyword</data> + </entity> + <entity name="ImageUploadPng" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">png.png</data> + <data key="file">png.png</data> + <data key="fileName">png</data> + <data key="extension">png</data> + </entity> + <entity name="ImageUploadGif" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">gif.gif</data> + <data key="file">gif.gif</data> + <data key="fileName">gif</data> + <data key="extension">gif</data> + </entity> + <entity name="ImageMetadata" type="image"> + <data key="title">Title of the magento image</data> + <data key="description">Description of the magento image</data> + <data key="file">magento3.jpg</data> + <data key="fileName">Title of the magento image</data> + <data key="extension">jpg</data> + <data key="keywords">magento, mediagallerymetadata</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml new file mode 100644 index 0000000000000..e4149acdf58d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.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="AdminMediaGalleryFolderData"> + <data key="name" unique="suffix">folder</data> + </entity> + <entity name="AdminMediaGalleryFolderInvalidData"> + <data key="name">,.?/:;'[{]}|~`!@#$%^*()_=+</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml new file mode 100644 index 0000000000000..e8f394a006104 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml @@ -0,0 +1,18 @@ +<?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="MediaGalleryConfigDataEnabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">1</data> + </entity> + <entity name="MediaGalleryConfigDataDisabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml new file mode 100644 index 0000000000000..f7ed27171db40 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminStandaloneMediaGalleryPage" url="/media_gallery/media" area="admin" module="Magento_MediaGalleryUi"/> +</pages> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml new file mode 100644 index 0000000000000..e0305a8a6f172 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.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="AdminConfigSystemSection"> + <element name="enhancedMediaGalleryFieldset" type="block" selector="#system_media_gallery-head"/> + <element name="enhancedMediaGalleryEnabledField" type="select" selector="[data-ui-id='select-groups-media-gallery-fields-enabled-value']"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml new file mode 100644 index 0000000000000..2feaaa8594da2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml @@ -0,0 +1,17 @@ +<?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="AdminEnhancedMediaGalleryActionsSection"> + <element name="upload" type="input" selector="#image-uploader-input"/> + <element name="cancel" type="button" selector="[data-ui-id='cancel-button']"/> + <element name="createFolder" type="button" selector="[data-ui-id='create-folder-button']"/> + <element name="deleteFolder" type="button" selector="[data-ui-id='delete-folder-button']"/> + <element name="imageSrc" type="text" selector="//div[@class='masonry-image-column' and contains(@data-repeat-index, '0')]//img[contains(@src,'{{src}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml new file mode 100644 index 0000000000000..b4071295bacf3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryDeleteModalSection"> + <element name="confirmDelete" type="button" selector=".media-gallery-delete-image-action .action-accept"/> + <element name="cancelDelete" type="button" selector=".media-gallery-delete-image-action .action-dismiss"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml new file mode 100644 index 0000000000000..b8e2f698ccfe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -0,0 +1,20 @@ +<?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="AdminEnhancedMediaGalleryEditDetailsSection"> + <element name="title" type="input" selector="#title"/> + <element name="fileName" type="text" selector="#path"/> + <element name="description" type="textarea" selector="#description"/> + <element name="newKeyword" type="input" selector="[data-ui-id='keyword']"/> + <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + <element name="save" type="button" selector="#image-details-action-save"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml new file mode 100644 index 0000000000000..32b109f1e0483 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml @@ -0,0 +1,29 @@ +<?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="AdminEnhancedMediaGalleryFiltersSection"> + <element name="filtersButton" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-expand']"/> + <element name="categoryGridFiltersButton" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-expand']"/> + <element name="sourceFilterValue" type="select" parameterized="true" selector="//div[@class='media-gallery-container']//select[@name='source']//option[@value='{{option}}']"/> + <element name="applyFilters" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-apply']"/> + <element name="categoryGridApplyFilters" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-apply']"/> + <element name="activeFilter" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']//span[contains( ., '{{filter}}')]" parameterized="true"/> + <element name="activeFilterPlaceholder" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']"/> + <element name="usedInSelectDropdown" type="text" selector="//label[@class='admin__form-field-label']/span[text()='Show Images Used In']/parent::*/parent::div/div//div[@class='admin__action-multiselect-text' and text()='Select...']"/> + <element name="usedInEntityType" type="text" selector="//label[@class='admin__action-multiselect-label']/span[text()='{{entityType}}']" parameterized="true"/> + <element name="usedInDoneButton" type="button" selector="//div[@class='admin__action-multiselect-actions-wrap']/button/span[text()='Done']"/> + <element name="selectFilter" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select" parameterized="true"/> + <element name="selectFilterOption" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select/option[@data-title='{{optionLabel}}']" parameterized="true"/> + <element name="searchOptionsFilter" type="select" selector="//div[label/span[contains(text(), '{{filterName}}')]]//div[@class='action-select admin__action-multiselect']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterInput" type="input" selector="//div[label/span[contains(text(), '{{filterName}}')]]//input[@data-role='advanced-select-text']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterOption" type="text" selector="//div[label/span[contains(text(), '{{filterName}}')]]//label[@class='admin__action-multiselect-label']/span[text()='{{optionName}}']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterDone" type="button" selector="//div[label/span[contains(text(), '{{filterName}}')]]//button[@data-action='close-advanced-select']" parameterized="true"/> + <element name="duplicatedFilterCheckbox" type="button" selector="//input[@name='duplicated']"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml new file mode 100644 index 0000000000000..3f13a57697e6f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -0,0 +1,17 @@ +<?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="AdminEnhancedMediaGalleryImageActionsSection"> + <element name="openContextMenu" type="button" selector=".three-dots"/> + <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> + <element name="edit" type="button" selector="[data-ui-id='action-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/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml new file mode 100644 index 0000000000000..32cd99bfe6b11 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.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="AdminEnhancedMediaGalleryImageDescriptionSection"> + <element name="title" type="text" selector=".masonry-image-description .name"/> + <element name="contentType" type="text" selector=".masonry-image-description .type"/> + <element name="dimensions" type="text" selector=".masonry-image-description .dimensions" /> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml new file mode 100644 index 0000000000000..a40a70c5f160c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -0,0 +1,16 @@ +<?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="AdminEnhancedMediaGalleryMassActionSection"> + <element name="massActionCheckbox" type="button" selector="//input[@type='checkbox'][@data-ui-id ='{{imageName}}']" parameterized="true"/> + <element name="totalSelected" type="text" selector=".mediagallery-massaction-items-count > .selected_count_text"/> + <element name="cancelMassActionMode" type="button" selector="#cancel_massaction"/> + <element name="deleteSelected" type="button" selector="#delete_massaction"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml new file mode 100644 index 0000000000000..0bcbeb0d7a00f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -0,0 +1,24 @@ +<?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="AdminEnhancedMediaGalleryViewDetailsSection"> + <element name="title" type="text" selector=".image-title"/> + <element name="contentType" type="text" selector="[data-ui-id='content-type']"/> + <element name="type" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Type')]/following-sibling::div"/> + <element name="height" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Height')]/following-sibling::div"/> + <element name="description" type="text" selector=".image-details-section.description p"/> + <element name="keywords" type="text" selector="//div[@class='tags-list']"/> + <element name="filename" type="text" selector=".image-details-section.filename p"/> + <element name="edit" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'edit')]"/> + <element name="delete" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'delete')]"/> + <element name="confirmDelete" type="button" selector=".action-accept"/> + <element name="addImage" type="button" selector=".add-image-action"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml new file mode 100644 index 0000000000000..4c9e6bf362194 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml @@ -0,0 +1,22 @@ +<?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="AdminMediaGalleryFolderSection"> + <element name="folderNewModalHeader" type="block" selector="//h1[contains(text(), 'New Folder Name')]"/> + <element name="folderDeleteModalHeader" type="block" selector="//h1[contains(text(), 'Are you sure you want to delete this folder?')]"/> + <element name="folderNewCreateButton" type="button" selector="#create_folder"/> + <element name="folderDeleteButton" type="button" selector="#delete_folder"/> + <element name="folderConfirmDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'OK')]"/> + <element name="folderCancelDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'Cancel')]"/> + <element name="folderNameField" type="button" selector="[name=folder_name]"/> + <element name="folderConfirmCreateButton" type="button" selector="//button/span[contains(text(),'Confirm')]"/> + <element name="folderNameValidationMessage" type="block" selector="label.mage-error"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml new file mode 100644 index 0000000000000..9271c0ff61618 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.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="AdminMediaGalleryHeaderButtonsSection"> + <element name="addSelected" type="button" selector=".media-gallery-add-selected"/> + <element name="deleteSelected" type="button" selector="#delete_selected"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml new file mode 100644 index 0000000000000..4749fc4a885b0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MediaGalleryUiSuite"> + <before> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYG" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="enableEnhancedMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYG" /> + </after> + <include> + <group name="media_gallery_ui"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml new file mode 100644 index 0000000000000..94831b039b53a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml @@ -0,0 +1,50 @@ +<?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="AdminEnhancedMediaGalleryDeleteImagesInBulkTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1488"/> + <title value="User deletes images with less clicks"/> + <stories value="[Story #42] User deletes images in bulk"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User deletes images with less clicks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToVerifyMode"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup" stepKey="assertMassActionModeAvailable"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup" stepKey="disableMassActionMode"/> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup" stepKey="assertImagesDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup" stepKey="assertMassectionModeDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml new file mode 100644 index 0000000000000..52f3a8079e962 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml @@ -0,0 +1,55 @@ +<?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="AdminEnhancedMediaGalleryDuplicatedImagesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1500"/> + <title value="User can filter duplicated images"/> + <stories value="[Story 59] User finds image duplicates"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User can filter duplicated images"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup" stepKey="SelectDuplicatedFilter"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertFirstImageInGrid"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertSecondImageInGrid"> + <argument name="title" value="ImageUpload_1.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml new file mode 100644 index 0000000000000..f026b87f7ec88 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml @@ -0,0 +1,70 @@ +<?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="AdminEnhancedMediaGalleryUploadImageWithMetadataTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="Magento extracts image meta data from file"/> + <stories value="Story 53 - Magento extracts image meta data from file"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="Magento extracts image meta data from file"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteJpegImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadPngImage"> + <argument name="image" value="ImageUploadPng"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewPngImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyPngImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyPngImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyPngImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deletePngImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadGifImage"> + <argument name="image" value="ImageUploadGif"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewGifImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyGifImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyGifImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyGifImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteGifImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml new file mode 100644 index 0000000000000..67bb09298893d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.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="AdminEnhancedMediaGalleryVerifyAssetFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1292"/> + <title value="User sees entities where asset is used in"/> + <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/4951024"/> + <description value="User sees entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryGridPage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml new file mode 100644 index 0000000000000..4719b98c78dbe --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml @@ -0,0 +1,79 @@ +<?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="AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1489"/> + <title value="User filters images that are not used in the content"/> + <stories value="Story 52: User filters images that are not used in the content"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images that are not used in the content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </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="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Not used anywhere"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml new file mode 100644 index 0000000000000..d54399bdeb2b2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml @@ -0,0 +1,83 @@ +<?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="AdminEnhancedMediaGalleryVerifyUsedInFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1567"/> + <title value="User filters images by the area they used in"/> + <stories value="User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images by the area they used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </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="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterIMage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Categories"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml new file mode 100644 index 0000000000000..cb7adf3307865 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml @@ -0,0 +1,78 @@ +<?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="AdminMediaGalleryAddCategoryImageFromTwoComponentsTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User add category image via wysiwyg and image uploader button"/> + <stories value="Story [54]: User inserts image rendition to the content with text area + Insert image button" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User add category image via wysiwyg and image uploader button"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload_1"/> + </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="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="reSaveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertContentImageIsVisible"> + <argument name="imageName" value="{{ImageUpload3.fileName}}"/> + </actionGroup> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertCategoryImageIsVisible"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml new file mode 100644 index 0000000000000..30f1412a5b08d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml @@ -0,0 +1,55 @@ +<?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="AdminMediaGalleryAddCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1073"/> + <title value="User add category image via wysiwyg"/> + <stories value="User add category image via wysiwyg"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4484351"/> + <description value="User add category image via wysiwyg"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertImageInCategoryDescriptionField"> + <argument name="imageName" value="{{ImageUpload3.fileName}}" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml new file mode 100644 index 0000000000000..94307fa510a50 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml @@ -0,0 +1,41 @@ +<?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="AdminMediaGalleryAddFromImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1229"/> + <stories value="[Story #38] User views basic image attributes in Media Gallery"/> + <title value="Adding image from the Image Details"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4569982"/> + <description value="Adding image from the Image Details"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup" stepKey="addImageFromViewDetails"/> + <actionGroup ref="AssertImageAddedToPageContentActionGroup" stepKey="assertImageAddedToContent"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..6e6f5240e84be --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml @@ -0,0 +1,57 @@ +<?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="AdminMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1046; https://github.com/magento/adobe-stock-integration/issues/1047"/> + <stories value="Creating, deleting new folder functionality in Media Gallery"/> + <title value="Creating, deleting new folder functionality in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4456547; https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4457075"/> + <description value="Creating, deleting new folder functionality in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml new file mode 100644 index 0000000000000..980d6b7c85c20 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml @@ -0,0 +1,33 @@ +<?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="AdminMediaGalleryDeleteImageContextMenuTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/710"/> + <title value="Uploading and deleting an image using context menu"/> + <stories value="[Story #52] User accesses Media Gallery from the main navigation"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="Uploading and deleting an image using context menu"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml new file mode 100644 index 0000000000000..ad364e7709a33 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml @@ -0,0 +1,41 @@ +<?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="AdminMediaGalleryDeleteImageFileTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1094"/> + <title value="Deleting new image file functionality in Enhanced Media Gallery"/> + <stories value="Deleting new image file functionality in Enhanced Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4756652"/> + <description value="Deleting new image file functionality in Enhanced Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="verifyImageIsDeleted"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml new file mode 100644 index 0000000000000..6ae8ed7047434 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml @@ -0,0 +1,56 @@ +<?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="AdminMediaGalleryDeleteImageWithWarningPopupTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1511"/> + <title value="User sees warning when deleting image if it's used on storefront"/> + <stories value="User sees warning when deleting image if it's used on storefront"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4843896"/> + <description value="User sees warning when deleting image if it's used on storefront"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToAssertMessage"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup" stepKey="assertMessageImageUsedIn"> + <argument name="messageText" value="The selected assets are used in the content of the following entities: Categories(1)"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml new file mode 100644 index 0000000000000..963a0b954e45b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -0,0 +1,65 @@ +<?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="AdminMediaGalleryDisabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by disabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by disabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="disableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Disabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..960443998d010 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryEditImageDetailsTest"> + <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"/> + <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"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <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/AdminMediaGalleryEnabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml new file mode 100644 index 0000000000000..c2b167912dda7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml @@ -0,0 +1,57 @@ +<?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="AdminMediaGalleryEnabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by enabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by enabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Enabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml new file mode 100644 index 0000000000000..e1e7bf1f0bcbb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml @@ -0,0 +1,53 @@ +<?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="AdminMediaGalleryFilterImagesBySourceTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1393"/> + <title value="User filters images by source filter"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4760144"/> + <description value="User filters images by source filter"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteContentImage"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilterToApplySourceFilter"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyAdobeStockFilter"> + <argument name="filterValue" value="Adobe Stock"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFiltersWithAdobeStockOption"/> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml new file mode 100644 index 0000000000000..b8ce1f76ad4c8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml @@ -0,0 +1,45 @@ +<?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="AdminMediaGallerySaveFiltersStateTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1397"/> + <title value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <stories value="User is able to use bookmarks controls in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4763040"/> + <description value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySaveCustomViewActionGroup" stepKey="saveCustomView"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup" stepKey="assertFilterApplied"> + <argument name="resultValue" value="Uploaded Locally"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup" stepKey="assertNoActiveFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeleteGridViewActionGroup" stepKey="deleteView"> + <argument name="viewToDelete" value="Test View"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml new file mode 100644 index 0000000000000..eceda879e5597 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml @@ -0,0 +1,62 @@ +<?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="AdminMediaGalleryStoreViewCategoryFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by category store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by category store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml new file mode 100644 index 0000000000000..86cae11267eaa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.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="AdminMediaGalleryStoreViewContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by content store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by content store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="createCMSPage" stepKey="deleteCmsPage"/> + </after> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSavePage"/> + <waitForPageLoad stepKey="waitForPageSave"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml new file mode 100644 index 0000000000000..ca7a71258fead --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -0,0 +1,43 @@ +<?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="AdminMediaGalleryUploadCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1435"/> + <stories value="User uploads image outside of the Media Gallery"/> + <title value="User uploads image outside of the Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> + <description value="User uploads image outside of the Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteCategoryImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ProductImage.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml new file mode 100644 index 0000000000000..01a26cce1b6fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml @@ -0,0 +1,37 @@ +<?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="AdminMediaGalleryVerifyImageGridAttributesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/708"/> + <title value="Verify image grid attributes"/> + <stories value="[Story #41] User views limited image information from the image grid in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/3839218"/> + <description value="User views basic image attributes in Media Gallery grid"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="assertImageAttributes"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml new file mode 100644 index 0000000000000..00fc07eb6c1af --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml @@ -0,0 +1,37 @@ +<?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="AdminMediaGalleryViewDetailsDeleteImageTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/461"/> + <title value="Deleting an image from view details panel"/> + <stories value="[Story #42] User deletes images"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4516773"/> + <description value="Deleting an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageMetadata"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..92909bcf06795 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,46 @@ +<?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="AdminMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <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="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <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/AdminMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..c9447d5cc8a52 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml @@ -0,0 +1,40 @@ +<?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="AdminMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..164ab523d508a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml @@ -0,0 +1,56 @@ +<?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="AdminStandaloneMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1119; https://github.com/magento/adobe-stock-integration/issues/1120"/> + <stories value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <title value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503041; https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503101"/> + <description value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..ede3a452e4ca5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.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="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <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"/> + <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"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <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/AdminStandaloneMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..2cf6bf5dfe623 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,55 @@ +<?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="AdminStandaloneMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from standalone view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from standalone view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminMediaGalleryEditAssetAddKeywordActionGroup" stepKey="setKeywords"> + <argument name="keyword" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyMetadataKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..bb7071497ce24 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml @@ -0,0 +1,40 @@ +<?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="AdminStandaloneMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details 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/1337102/scenarios/4503223"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..3d4e523d0d6b1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Config data test. + */ +class ConfigTest extends TestCase +{ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Config + */ + private $config; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->config = $this->objectManager->getObject( + Config::class, + [ + 'scopeConfig' => $this->scopeConfigMock + ] + ); + } + + /** + * Get Magento media gallery enabled test. + */ + public function testIsEnabled(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('isSetFlag') + ->with(self::XML_PATH_ENABLED) + ->willReturn(true); + $this->assertEquals(true, $this->config->isEnabled()); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php new file mode 100644 index 0000000000000..fc8a0756a7b55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\UploadImage; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Provides test for upload image functionality + */ +class UploadImageTest extends TestCase +{ + /** + * @var Storage|MockObject + */ + private $imagesStorageMock; + + /** + * @var Filesystem|MockObject + */ + private $fileSystemMock; + + /** + * @var Read|MockObject + */ + private $mediaDirectoryMock; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->imagesStorageMock = $this->createMock(Storage::class); + $this->fileSystemMock = $this->createMock(Filesystem::class); + $this->mediaDirectoryMock = $this->createMock(Read::class); + + $this->uploadImage = (new ObjectManager($this))->getObject( + UploadImage::class, + [ + 'imagesStorage' => $this->imagesStorageMock, + 'filesystem' => $this->fileSystemMock, + ] + ); + } + + /** + * Test successful image file upload. + * + * @param string $targetFolder + * @param string|null $type + * @param string $absolutePath + * + * @dataProvider executeDataProvider + */ + public function testExecute(string $targetFolder, string $type = null, string $absolutePath): void + { + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(true); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with($targetFolder) + ->willReturn($absolutePath); + + $uploadResult = ['path' => 'media/catalog', 'file' => 'test-image.jpeg']; + $this->imagesStorageMock->expects($this->once()) + ->method('uploadFile') + ->with($absolutePath, $type) + ->willReturn($uploadResult); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Test upload image method with logical exception when the folder is not a folder. + */ + public function testExecuteWithException(): void + { + $targetFolder = 'not-a-folder'; + $type = 'image'; + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(false); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Directory not-a-folder does not exist in media directory.'); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Provides test case data. + * + * @return array + */ + public function executeDataProvider(): array + { + return [ + [ + 'targetFolder' => 'media/catalog', + 'type' => 'image', + 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5244f8dc420e188df576181f313075c73b176d13 GIT binary patch literal 58077 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 A9smFU literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5244f8dc420e188df576181f313075c73b176d13 GIT binary patch literal 58077 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 A9smFU literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php new file mode 100644 index 0000000000000..4047a4fcb98d8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Directories tree component + */ +class DirectoriesTree extends Container +{ + /** + * @var UrlInterface + */ + private $url; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'getDirectoryTreeUrl' => $this->url->getUrl("media_gallery/directories/gettree"), + 'deleteDirectoryUrl' => $this->url->getUrl("media_gallery/directories/delete"), + 'createDirectoryUrl' => $this->url->getUrl("media_gallery/directories/create") + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php new file mode 100644 index 0000000000000..ad5e27381dee2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\File\Size; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Image Uploader component + */ +class ImageUploader extends Container +{ + private const ACCEPT_FILE_TYPES = '/(\.|\/)(gif|jpe?g|png)$/i'; + private const ALLOWED_EXTENSIONS = 'jpg jpeg png gif'; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @var Size + */ + private $size; + + /** + * @param Size $size + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + Size $size, + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->size = $size; + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'imageUploadUrl' => $this->url->getUrl('media_gallery/image/upload', ['type' => 'image']), + 'acceptFileTypes' => self::ACCEPT_FILE_TYPES, + 'allowedExtensions' => self::ALLOWED_EXTENSIONS, + 'maxFileSize' => $this->size->getMaxFileSize() + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php new file mode 100644 index 0000000000000..1fc5a80960a69 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +/** + * Image Uploader component + */ +class ImageUploaderStandAlone extends ImageUploader +{ + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'actionsPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_columns.thumbnail_url', + 'directoriesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_directories', + 'messagesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing.messages' + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php new file mode 100644 index 0000000000000..aec99bf7d3b8c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Asset\Repository as AssetRepository; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Source icon url provider + */ +class SourceIconProvider extends Column +{ + /** + * @var array + */ + private $sourceIcons; + + /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param AssetRepository $assetRepository + * @param ScopeConfigInterface $scopeConfig + * @param array $components + * @param array $data + * @param array $sourceIcons + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + AssetRepository $assetRepository, + ScopeConfigInterface $scopeConfig, + array $components = [], + array $data = [], + array $sourceIcons = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->assetRepository = $assetRepository; + $this->scopeConfig = $scopeConfig; + $this->sourceIcons = $sourceIcons; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items']) && is_iterable($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')] = $item[$this->getData('name')] + ? $this->getSourceIconUrl($item[$this->getData('name')]) + : null; + } + } + + return $dataSource; + } + + /** + * Construct source icon url based on the source code matching + * + * @param string $sourceName + * + * @return string|null + */ + public function getSourceIconUrl(string $sourceName): ?string + { + return isset($this->sourceIcons[$sourceName]) + ? $this->assetRepository->getUrlWithParams( + $this->sourceIcons[$sourceName], + ['_secure' => $this->isSecure()] + ) + : null; + } + + /** + * Check if store use secure connection + * + * @return bool + */ + private function isSecure(): bool + { + return $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php new file mode 100644 index 0000000000000..481f8ab861f0f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Backend\Model\UrlInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Overlay column + */ +class Url extends Column +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * UrlInterface $urlInterface + */ + private $urlInterface; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param UrlInterface $urlInterface + * @param Images $images + * @param Storage $storage + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + UrlInterface $urlInterface, + Images $images, + Storage $storage, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->storeManager = $storeManager; + $this->urlInterface = $urlInterface; + $this->images = $images; + $this->storage = $storage; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + * @throws NoSuchEntityException + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + $item['encoded_id'] = $this->images->idEncode($item['path']); + $item[$this->getData('name')] = $this->getUrl($item[$this->getData('name')]); + } + } + + return $dataSource; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'), + 'storeId' => $this->storeManager->getStore()->getId() + ] + ) + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws NoSuchEntityException + */ + private function getUrl(string $path): string + { + return $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $path); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php new file mode 100644 index 0000000000000..273cf9e37554b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; + +/** + * Asset filter + */ +class Asset extends Select +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param GetContentByAssetIdsInterface $getContentIdentities + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + GetContentByAssetIdsInterface $getContentIdentities, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->getContentIdentities = $getContentIdentities; + } + + /** + * Apply filter + * + * @return void + */ + public function applyFilter() + { + if (isset($this->filterData[$this->getName()])) { + $ids = is_array($this->filterData[$this->getName()]) + ? $this->filterData[$this->getName()] + : [$this->filterData[$this->getName()]]; + $filter = $this->filterBuilder->setConditionType('in') + ->setField($this->_data['config']['identityColumn']) + ->setValue($this->getEntityIdsByAsset($ids)) + ->create(); + + $this->getContext()->getDataProvider()->addFilter($filter); + } + } + + /** + * Return entity ids by assets ids. + * + * @param array $ids + */ + private function getEntityIdsByAsset(array $ids): string + { + if (!empty($ids)) { + $categoryIds = []; + $data = $this->getContentIdentities->execute($ids); + foreach ($data as $identity) { + if ($identity->getEntityType() === $this->_data['config']['entityType']) { + $categoryIds[] = $identity->getEntityId(); + } + } + return implode(',', $categoryIds); + } + return ''; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php new file mode 100644 index 0000000000000..31c658a6c4208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Status filter options + */ +class Status implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + ['value' => '1', 'label' => __('Enabled')], + ['value' => '0', 'label' => __('Disabled')] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php new file mode 100644 index 0000000000000..cf49377c19837 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; + +/** + * Store Options for content field + */ +class Store extends StoreOptions +{ + /** + * All Store Views value + */ + const ALL_STORE_VIEWS = '0'; + + /** + * Get options + * + * @return array + */ + public function toOptionArray() + { + if ($this->options !== null) { + return $this->options; + } + + $this->currentOptions['All Store Views']['label'] = __('All Store Views'); + $this->currentOptions['All Store Views']['value'] = self::ALL_STORE_VIEWS; + + $this->generateCurrentOptions(); + + $this->options = array_values($this->currentOptions); + + return $this->options; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php new file mode 100644 index 0000000000000..d3f758a510888 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Used in filter options + */ +class UsedIn 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/MediaGalleryUi/Ui/Component/Listing/Provider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php new file mode 100644 index 0000000000000..160097967165d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\MediaGalleryUi\Ui\Component\Listing; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Psr\Log\LoggerInterface as Logger; + +class Provider extends SearchResult +{ + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param GetAssetsKeywordsInterface $getAssetKeywords + * @param string $mainTable + * @param null|string $resourceModel + * @param null|string $identifierName + * @param null|string $connectionName + * @throws LocalizedException + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + GetAssetsKeywordsInterface $getAssetKeywords, + $mainTable = 'media_gallery_asset', + $resourceModel = null, + $identifierName = null, + $connectionName = null + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName, + $connectionName + ); + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * @inheritdoc + */ + public function getData() + { + $data = parent::getData(); + $keywords = []; + foreach ($this->_items as $asset) { + $keywords[$asset->getId()] = array_map(function (AssetKeywordsInterface $assetKeywords) { + return array_map(function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, $assetKeywords->getKeywords()); + }, $this->getAssetKeywords->execute([$asset->getId()])); + } + + /** @var AssetInterface $asset */ + foreach ($data as $key => $asset) { + $data[$key]['thumbnail_url'] = $asset['path']; + $data[$key]['content_type'] = strtoupper(str_replace('image/', '', $asset['content_type'])); + $data[$key]['preview_url'] = $asset['path']; + $data[$key]['keywords'] = isset($keywords[$asset['id']]) ? implode(",", $keywords[$asset['id']]) : ''; + $data[$key]['source'] = empty($asset['source']) ? __('Local') : $asset['source']; + } + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json new file mode 100644 index 0000000000000..f4701306eb369 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-media-gallery-ui", + "description": "Magento module responsible for the media gallery UI implementation", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/module-store": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..bf07512d13d4f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml @@ -0,0 +1,70 @@ +<?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\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField"> + <arguments> + <argument name="getAssetIdsByContentStatus" xsi:type="object">Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="path" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Directory</item> + <item name="fulltext" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Keyword</item> + <item name="entity_type" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\EntityType</item> + <item name="duplicated" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Duplicated</item> + <item name="content_status" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + <item name="store_id" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\SortingProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="filters" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor</item> + <item name="sorting" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor</item> + <item name="pagination" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor</item> + <item name="joins" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\Listing\DataProvider"> + <arguments> + <argument name="collectionProcessor" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"> + <arguments> + <argument name="options" xsi:type="array"> + <item name="cms_page" xsi:type="array"> + <item name="value" xsi:type="string">cms_page</item> + <item name="label" xsi:type="string" translate="true">Pages</item> + </item> + <item name="catalog_category" xsi:type="array"> + <item name="value" xsi:type="string">catalog_category</item> + <item name="label" xsi:type="string" translate="true">Categories</item> + </item> + <item name="cms_block" xsi:type="array"> + <item name="value" xsi:type="string">cms_block</item> + <item name="label" xsi:type="string" translate="true">Blocks</item> + </item> + <item name="catalog_product" xsi:type="array"> + <item name="value" xsi:type="string">catalog_product</item> + <item name="label" xsi:type="string" translate="true">Products</item> + </item> + <item name="not_used" xsi:type="array"> + <item name="value" xsi:type="string">not_used</item> + <item name="label" xsi:type="string" translate="true">Not used anywhere</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..92839aa75ac8b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml @@ -0,0 +1,13 @@ +<?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_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_Cms::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> + <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_Cms::media_gallery"/> + </menu> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..11a555e16e957 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?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:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery" frontName="media_gallery"> + <module name="Magento_MediaGalleryUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..77544b42e899a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enhanced Media Gallery</label> + <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>system/media_gallery/enabled</config_path> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/config.xml b/app/code/Magento/MediaGalleryUi/etc/config.xml new file mode 100644 index 0000000000000..fe8e73c406e59 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/config.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery> + <enabled>0</enabled> + </media_gallery> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml new file mode 100644 index 0000000000000..040e003817efa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryUiApi\Api\ConfigInterface" type="Magento\MediaGalleryUi\Model\Config"/> + <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> + <arguments> + <argument name="collections" xsi:type="array"> + <item name="media_gallery_listing_data_source" xsi:type="string">Magento\MediaGalleryUi\Ui\Component\Listing\Provider</item> + </argument> + </arguments> + </type> + <virtualType name="mediaGallerySearchResult" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult"> + <arguments> + <argument name="mainTable" xsi:type="string">media_gallery_asset_grid</argument> + <argument name="resourceModel" xsi:type="string">Magento\MediaGalleryUi\Model\ResourceModel\Grid\Asset</argument> + </arguments> + </virtualType> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="resizeParameters" xsi:type="array"> + <item name="height" xsi:type="number">200</item> + <item name="width" xsi:type="number">200</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Model\Directories\FolderTree"> + <arguments> + <argument name="path" xsi:type="string">media</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\Type"> + <arguments> + <argument name="types" xsi:type="array"> + <item name="image" xsi:type="string">Image</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <plugin name="createMediaGalleryThumbnails" type="Magento\MediaGalleryUi\Plugin\CreateThumbnails"/> + </type> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProviderPool"> + <arguments> + <argument name="detailsProviders" xsi:type="array"> + <item name="10" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Type</item> + <item name="20" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\CreatedAt</item> + <item name="30" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\UpdatedAt</item> + <item name="40" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Width</item> + <item name="50" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Height</item> + <item name="60" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Size</item> + <item name="70" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/module.xml b/app/code/Magento/MediaGalleryUi/etc/module.xml new file mode 100644 index 0000000000000..0deede3e6aad0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryUi"> + <sequence> + <module name="Magento_Cms" /> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryUi/i18n/en_US.csv b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv new file mode 100644 index 0000000000000..1882665ce8033 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv @@ -0,0 +1,8 @@ +"Enhanced Media Gallery","Enhanced Media Gallery" +Enabled,Enabled +All,All +Directory,Directory +"Uploaded Date","Uploaded Date" +"Modification Date","Modification Date" +Overlay,Overlay +"Thumbnail Image","Thumbnail Image" diff --git a/app/code/Magento/MediaGalleryUi/registration.php b/app/code/Magento/MediaGalleryUi/registration.php new file mode 100644 index 0000000000000..e1d321c5a8ff3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUi', __DIR__); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml new file mode 100644 index 0000000000000..f41c0f91b2249 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <container name="root"> + <block name="media.gallery.container" + class="Magento\Backend\Block\Template" + template="Magento_MediaGalleryUi::container.phtml" + aclResource="Magento_Cms::media_gallery"> + <container name="gallery.actions" htmlTag="div" htmlClass="page-main-actions"> + <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> + </container> + <uiComponent name="media_gallery_listing"/> + <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details.phtml"> + <arguments> + <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + </arguments> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </block> + </container> +</layout> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml new file mode 100644 index 0000000000000..7750f22b39ce7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> + <uiComponent name="standalone_media_gallery_listing"/> + <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <arguments> + <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + </arguments> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details_standalone.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml new file mode 100644 index 0000000000000..5b905ea97d64a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength + +?> + +<div class="media-gallery-container"> + <?= $block->getChildHtml(); ?> +</div> + +<script type="text/x-magento-init"> + { + ".media-gallery-container": { + "Magento_Ui/js/core/app": { + "components": { + "media_gallery_container": { + "component": "Magento_MediaGalleryUi/js/container", + "containerSelector": ".media-gallery-container", + "masonryComponentPath": "media_gallery_listing.media_gallery_listing.media_gallery_columns" + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml new file mode 100644 index 0000000000000..ba2033478afa1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; +use Magento\Framework\Escaper; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var Escaper $escaper */ + +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-image-actions" + data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "media_gallery_listing.media_gallery_listing.messages" + } + } + } + }, + "#media-gallery-image-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", + "handler": "deleteImageAction", + "name": "delete", + "classes": "action-default scalable delete action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", + "handler": "addImage", + "name": "add-image", + "classes": "scalable action-primary add-image-action" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml new file mode 100644 index 0000000000000..9fc0e749ac888 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-image-actions" + data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + } + }, + "#media-gallery-image-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", + "handler": "deleteImageAction", + "name": "delete", + "classes": "action-default scalable delete action-quaternary" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml new file mode 100644 index 0000000000000..c2b7e66cc89bd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-edit-image-actions" + data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + }, + "#media-gallery-image-edit-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-edit-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml new file mode 100644 index 0000000000000..ec48ed8bb9053 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-edit-image-actions" + data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + }, + "#media-gallery-image-edit-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-edit-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml new file mode 100644 index 0000000000000..86c8590bb4860 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">cms_block</item> + <item name="identityColumn" xsi:type="string">block_id</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml new file mode 100644 index 0000000000000..58881a8c9de6c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">cms_page</item> + <item name="identityColumn" xsi:type="string">page_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..5a16ed1792159 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,393 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <buttons> + <button name="add_selected"> + <param name="on_click" xsi:type="string">return false;</param> + <param name="sort_order" xsi:type="number">110</param> + <class>action-primary no-display media-gallery-add-selected</class> + <label translate="true">Add Selected</label> + </button> + <button name="cancel"> + <param name="on_click" xsi:type="string">MediabrowserUtility.closeDialog();</param> + <param name="sort_order" xsi:type="number">1</param> + <class>cancel action-quaternary</class> + <label translate="true">Cancel</label> + </button> + <button name="upload_image"> + <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> + <class>action-add scalable media-gallery-actions-buttons</class> + <param name="sort_order" xsi:type="number">20</param> + <label translate="true">Upload Image</label> + </button> + <button name="delete_folder"> + <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> + <param name="disabled" xsi:type="string">disabled</param> + <param name="sort_order" xsi:type="number">30</param> + <class>action-default scalable media-gallery-actions-buttons</class> + <label translate="true">Delete Folder</label> + </button> + <button name="create_folder"> + <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> + <param name="sort_order" xsi:type="number">10</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Create Folder</label> + </button> + <button name="delete_massaction"> + <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> + <param name="sort_order" xsi:type="number">50</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Images...</label> + </button> + </buttons> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + media_gallery_listing.media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <container name="media_gallery_massactions" + displayArea="sorting" + sortOrder="10" + component="Magento_MediaGalleryUi/js/grid/massaction/massactions" + template="Magento_MediaGalleryUi/grid/massactions/count" > + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + template="Magento_MediaGalleryUi/grid/directories/directoryTree" + component="Magento_MediaGalleryUi/js/directory/directoryTree"/> + <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">media_gallery_listing.media_gallery_listing.messages</item> + <item name="imageModelname" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploader" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml new file mode 100644 index 0000000000000..2b7d9fde3b9ff --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">catalog_product</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..c96ad0fd86661 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,380 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + standalone_media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + <buttons> + <button name="delete_folder"> + <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> + <param name="disabled" xsi:type="string">disabled</param> + <param name="sort_order" xsi:type="number">20</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Folder</label> + </button> + <button name="create_folder"> + <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> + <param name="sort_order" xsi:type="number">30</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Create Folder</label> + </button> + <button name="delete_massaction"> + <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> + <param name="sort_order" xsi:type="number">50</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Images...</label> + </button> + <button name="upload_image"> + <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Upload Image</label> + </button> + </buttons> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="standalone_media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <container name="media_gallery_massactions" + displayArea="sorting" + sortOrder="10" + component="Magento_MediaGalleryUi/js/grid/massaction/massactions" + template="Magento_MediaGalleryUi/grid/massactions/count" > + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">standalone_media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + template="Magento_MediaGalleryUi/grid/directories/directoryTree" + component="Magento_MediaGalleryUi/js/directory/directoryTree"/> + <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="url" xsi:type="string">thumbnail_url</item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.messages</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploaderStandAlone" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..671a82dce3f58 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,478 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// +// Variables +// _____________________________________________ + +@color-folders-background: #a6a6a6; +@color-folders-background-selected: #cdecf6; +@color-folders-border: #7185f5; +@color-masonry-overlay: #d9631c; +@color-masonry-grey: #9e9e9e; +@color-masonry-white: #e1e1e1; +@color-masonry-steelblue: #4682b4; +@color-media-gallery-buttons-background: #e3e3e3; +@color-media-gallery-buttons-border: #adadad; +@color-media-gallery-buttons-text: #514943; +@color-media-gallery-checkbox-background: #eee; + +& when (@media-common = true) { + + .media-gallery-delete-image-action, + .delete-folder-confirmation-popup { + + .modal-content { + word-wrap: anywhere; + } + } + + .media-gallery-asset-ui-select-filter { + + .admin__action-multiselect-crumb { + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis + } + + .admin__action-multiselect-label > span { + display: block; + margin-top: -2px; + max-height: 18px; + max-width: 70%; + overflow: hidden; + padding-left: 23px; + position: absolute; + text-overflow: ellipsis; + } + + .admin__action-multiselect-item-path { + float: right; + max-height: 70px; + max-width: 70px; + } + + .admin__action-multiselect-label { + display: inline-block; + width: 100%; + } + } + + .page-actions-buttons > button.no-display { + display: none; + } + + .page-actions-buttons > button.media-gallery-actions-buttons, + .page-actions .page-actions-buttons > button.media-gallery-actions-buttons:focus, + .page-actions-buttons > button.media-gallery-actions-buttons:hover { + background-color: @color-media-gallery-buttons-background; + border-color: @color-media-gallery-buttons-border; + color: @color-media-gallery-buttons-text; + } + + .mediagallery-massaction-checkbox { + background-color: @color-media-gallery-checkbox-background; + border-radius: 4px; + height: 40px; + input[type='checkbox'] { + margin-left: 10px; + margin-top: 11px; + } + margin-left: 15px; + margin-top: 10px; + position: absolute; + width: 40px; + z-index: 10; + } + + .mediagallery-massaction-items-count { + display: inline-block; + margin-left: -15px; + padding-right: 20px; + } + + .media-gallery-container { + + .masonry-image-grid .no-data-message-container, + .masonry-image-grid .error-message-container { + left: 50%; + margin-right: -50%; + position: sticky; + top: 50%; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + + .page-main-actions { + .page-actions { + .media-gallery-add-selected { + order: unset; + } + } + + & > .page-actions { + & > button.no-display { + display: none; + } + } + } + .jstree-default .jstree-hovered { + background: @color-folders-background; + border-color: @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .jstree-default .jstree-leaf a .jstree-icon { + background-position: -52px -16px; + } + + + .jstree-default a .jstree-icon { + background-position: -52px -16px; + } + + .jstree-default .jstree-no-dots .jstree-open > a > ins { + background-position: -52px -38px; + height: 20px; + width: 29px; + } + + .jstree a > ins { + float: left; + height: 22px; + margin-top: -3px; + width: 20px; + } + + .jstree-default .jstree-no-dots .jstree-leaf > ins { + background-image: none; + } + + .jstree-default ins { + background-image: url("@{baseDir}Magento_MediaGalleryUi/images/d.png"); + } + + .jstree a { + height: 30px; + margin: 1px; + padding-left: 6px; + padding-top: 6px; + width: 100%; + } + + .jstree-default .jstree-clicked { + background: @color-folders-background-selected; + border: .14em solid @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .masonry-image-overlay { + background-color: @color-masonry-overlay; + float: right; + font-size: 11px; + margin-left: 120px; + margin-top: 170px; + padding: .3rem; + pointer-events: none; + position: relative; + } + + .media-gallery-image-details { + float: left; + list-style: none; + margin-bottom: 0; + position: absolute; + width: 89%; + + .name { + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + display: -webkit-box; + font-size: 15px; + font-weight: bold; + line-height: 20px; + max-height: 50px; + overflow: hidden; + padding-bottom: 2px; + text-overflow: ellipsis; + white-space: pre-line; + word-wrap: anywhere; + word-wrap: break-word; + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + white-space: nowrap; + } + } + + .type { + display: inline-block; + font-size: 12px; + padding-bottom: 5px; + } + + .dimensions { + display: inline-block; + } + + .source { + display: inline-block; + } + } + + .media-gallery-image-actions { + float: right; + position: absolute; + right: 0; + width: 10%; + + .action-select-wrap { + cursor: pointer; + } + + .three-dots { + &:before { + content: url("@{baseDir}Magento_MediaGalleryUi/images/3-dots.png"); + cursor: pointer; + } + } + } + + .media-gallery-image { + height: 200px; + margin: 0 auto; + position: relative; + text-align: center; + width: 200px; + } + + .masonry-image-description { + background-color: @color-white; + min-height: 90px; + padding-top: 10px; + position: relative; + } + + .masonry-image-column { + background-color: @color-masonry-white; + width: 200px; + } + + .media-directory-container { + float: left; + padding-right: 40px; + } + + .media-gallery-image-block { + cursor: pointer; + height: 200px; + margin: 0 auto; + position: relative; + + &.selected { + border: 5px solid @color-masonry-steelblue; + } + } + + .media-gallery-image { + img { + bottom: 0; + height: auto; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + padding: 5px; + position: absolute; + right: 0; + top: 0; + width: auto; + } + + .action-menu { + bottom: 0; + float: right; + left: auto; + top: auto; + z-index: 100; + } + } + + .adobe-stock-icon { + margin-bottom: -6px; + width: 29px; + } + + .masonry-image-grid { + align-items: first baseline; + display: grid; + grid-template-columns: repeat(auto-fill, 210px); + justify-content: end; + margin: 10px 0; + position: relative; + } + + .admin__data-grid-filters .admin__form-field { + .action-select-wrap { + .action-menu { + width: 110%; + } + .admin__action-multiselect-search-label { + right: 1.5rem; + } + } + + .action-close { + padding: 0; + &:before { + font-size: 6px; + } + } + } + } + + .media-gallery-image-details-modal, + .media-gallery-edit-image-details-modal { + + .admin__action-multiselect-crumb { + .action-close { + padding: 0; + + &:before { + font-size: .5em; + } + } + } + + .edit-image-details { + padding: 50px; + } + + .path-display { + margin-top: 8px; + } + + .page-action-buttons { + float: right; + } + + .image-type { + .adobe-stock-icon { + margin-bottom: -6px; + width: 29px; + } + + .type { + color: @color-very-dark-gray; + } + } + + .image-details { + .lib-vendor-prefix-display(); + + .image-details-image { + img { + max-height: 650px; + } + } + + .image-details-sidebar { + .lib-vendor-prefix-flex-grow(1); + margin-top: 0; + padding-left: 40px; + + .image-details-section { + margin-bottom: 40px; + max-width: 400px; + min-width: 290px; + word-wrap: anywhere; + .lib-clearfix(); + } + + h3.image-title { + font-weight: bold; + line-height: 1.5; + } + + .attributes { + .attribute { + &:not(:last-child) { + margin-bottom: 20px; + padding-bottom: 20px; + } + + & > * { + float: left; + width: 50%; + } + + .value { + display: inline; + float: right; + } + + .title { + color: @color-very-dark-gray; + } + } + } + + .tags { + .tags-list { + margin-bottom: 10px; + + .show-more-item { + display: none; + } + + &.show-all-tags { + margin-bottom: 0; + + .show-more-item { + display: inline; + } + + & + .show-more-link-container { + display: none; + } + } + } + } + } + } + } + .masonry-image-sortby { + display: inline-block; + } + + .masonry-results-number { + display: inline-block; + margin-right: 1.4rem; + } +} + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .media-gallery-image-details-modal { + .image-details { + display: block; + + .image-details-sidebar { + margin-top: 20px; + padding-left: 0; + } + + .image-details-image img { + max-height: 450px; + } + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png new file mode 100644 index 0000000000000000000000000000000000000000..601ba415f2446038f3e34cda7cc503329e227fa7 GIT binary patch literal 3533 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 Hu0mjfJiWe5 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png new file mode 100644 index 0000000000000000000000000000000000000000..db5cda9c5512b899cf82293dfb5bf1ba7da831cd GIT binary patch literal 359 zcmV-t0hs=YP)<h;3K|Lk000e1NJLTq001Wd000~a0{{R3)xcJ10000#P)t-se}8}d z{r&3d>Zquwii(Qm<>lAc*Z=?j`uh6L&d#5opUKI|d3kx?-`{d_a`W@^l9H0RxVV~{ zn#9D!($dm3gMRS<0084jL_t(I%e~Xtj)O1^1yDPy19mp}|8K3KQq@YBjx?jWFAz9N z!UZ57LAuK!;AE|=W{SLAj+4O%hxF!_#TnT?ozK_7@-s)}H}fI2Ba{)JbzD2`X9nm# zW{SJmt_?o=2_;L1&2|#li=<UBg|MD0U%I_^!RMHHj%c~W>|Mj-q*m^`<(%gVBT{Y~ z=&_l-I1ef}(w*wV+`JDI4+_6Jy^(rd@ZM1)#5U@rdzbnqZtd)KT^Nn*UaE#?)C;wq zGhX*HIzS&zcaSDb`M2-y59hOf^7YfrVr0BKw}6>`x(8}U9pcx(xCa0L002ovPDHLk FV1nU$w3q+@ literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png new file mode 100644 index 0000000000000000000000000000000000000000..6516e915624c3348d83b2c4bb35a1e14bb2e20cb GIT binary patch literal 12159 zcmV-_FM!aAP)<h;3K|Lk000e1NJLTq003+N002k`0{{R3B!%GL001MIdQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3;uk{mgbrT^m;a|Emk;5b-C<_2^8`5r9DWL9PO zREtbTk`eT9cZ2Ej76sg$|MS1E`(OOml9|h;HtS}d|8mbg4t{9<`>(&hgU|2p_n)nw z--W+F?((k}MIK6gPv1Z5_<8=|@$%OTemy<Rzd!EU&u?P;d840S{CdNrD@T6Ve=d^8 z<M(*TKX25}&ystqe=c>#@B8|B;#<nkpH$yl|9%tw+x_`Ku0&}Uo|R%eDJ1{?Zr=*h z_h-ECgFlVuh4*t#&GD_IU-wEt{&su6zI*>?fc_TbUq88jjs6(_9Q-(bo{|0~mi7}P ze)z{<g!0e9e<A+k!s+*l;@^K!Yq<Y*J3oK!-R<7<-QCDWMD3@dy!#a$y@n@FLZ05s zjIYX{!q4@6b-tP(evzE*m!EvC&_g6E*M%H*7~zKVzOS%YVvZ+PzQ?#?dXKeKV~;B- zPg!5##+rKCsgbu8k4uS%<L|YEcisN3w?gB}JMh#Pcr&H?zx;Cl-7o(qU+!LYVF(K5 z_)^TeqAHhVD0BLoXOWPfG^oeIA74M|_kR=?rAh|N3v=TJho9eD%o6^pt@QGocwXWA z*A>Cl{qqKdh-()X6A~Hl7E%coe2uY&KpYzh8mv5~948qFrNqr5V@}DUtC5!B+PtQQ z_uN>bMLi5Qk%*EkRdUnQAXzyV>Zj&L4GnrLmr|_M(p>3fSh8%z%&H0GMol%>Qmxk7 zYOkZEmRo7oYHO{x(PK|wVCmLtZ@u?1q8nUj@Lq%G4<4CurkQ7%HtTG&&#@?<l~-A| z>T0X6vExn~nAo=KZoBVsf;%9^$)_AU^|aH^xYXKBH{WvY*4u8s<9F76vifgd|AVZB zpRC1~DLt?L&Ki%pT7SJo5S$d{jEuz`$aqx-DCnr1`4)1H%A9iMd!#8!WRXR=aXTnu zq%fZl%MHJC_gCirqrAD&{-eCb|5xUmQuqHObB@&gd*1#bYg-V!UdFyIR7`!M`*_Y6 z+l^N43d0KP<VND27TXQ8o$Sxx&ppS++QoK`Ys8&x4OZKG2_w~IISrDt9H{};20W6v zIUCo?m@8VDx%a>lL^||S%{Rxo39VMl4>3?0sw$fy?d5WvCli#$T7r?8HE71Qatkx_ zj9zz~n?|j>oISZpD056>UDIk?;Uo%SHWv<@I6NKQ2LET+vIOFFheAF2Z8le)HN?Gp zKgLY~yFE8MzLn5xga9ZGcS62KAaCZ4W>j`3kL=jah|$(cGwc=i*sGOwD$AIqdY&=P z869k=_6C^U$EJ9RNQ$}7Hp=Qg?9#%Xy`)B{@0s~5LTOv8MZS<zqSH1(in-OY@13-W z9%KRS5UkuOy<Qrn6(eysmNn0u_q~d||GbXw(+hvSEcFW0q5vv1a&|bE4qW3!3k2u; zF4rGx@1-=cP3mtHiYl$fx@tWawFU)ER|sV0(Y$IQRO_1W*DTYh=5Ie`-JG^j{__%& zzIj?MYi{b#EX_g3?CX(!fH*}@{RIR{2)CnahJ=?^B=L|Ny}R%1q0d9uR<(4%m7T$v zZZ$%yqdS8Iy&``zFKdTpBQ=H8=eA~FDV<7~Xw&xVekJZ!7j3?)^W|%Cz{fuXn0~%> z*JFDwdM=NeC$9{Puqs~Gu0*Dlmq(zpy1PLT3Sb1Ne0ja56u*tYg|UnhC=?d4YCYLq zds2Lo0GpMNG@q!gWobB?bP};?z1#K6g6-Xq%N>}ZNlUMyEBb&-P*W@El4Rs`o3?hO z(+|zQ+@^~!5@S`aVuUliR4+QXp-4kpxA#O3@^_ao0YO(M(uAPUWB$9lhR~WoZ`&yG zT4qD-oD)MYhBg_4MVyqtipG_qQR2$vjns}GhhDE|f*d=ekF2#d_&>77LjC2-C2LK4 z%uUGh^|z4BP9Z?kvGz&-mqVw|9y7r}NbwYx%3zs)tnnW^>F<q1^g^{x@>?!c>TIpJ z<7+;2LII5JwF1{k<GdHrWEYr$2X2&K$R}>Px;1FyR%x{x*Unmjqr1=mVDCwnYwex* zz5!{XQO5zugh}<SGNyC-YhDolUTy?mQ6Gm+cU##&EEtyeQ9CYTH#hiC3unST2#OJI zxrqpIlHCS^UCQ`;@GY7FA$Nz6Cy0mK9Qn}Wxv{!9F6*SguQazEBHp>-TwtLQ1Mt=% zKZFwuH&$!TU@T|@XfBOF=vp){E2daFmp++ac3OLt^b21O^pNgQ<#d+AJE2D8yK+_Q zakq`2P{iTHj(efA{Ati8LGyFGL>wYQF}$MXatQbk5us<eHiJUZ)MyfbbqzX=N{_5p zWCF-;!o5ik46rDL1-j951Ni~-D_f+^$zmN?B|Q&{j=;86#KD0T#yV*2sH*mTr-6F- zK%wltM8yvjQ2uCem+!TYNB+QsyI~|A?RRDmA&pC4h0;%}2NLU)`O)of3<-=(5#k`z zK(~Gch!fz#lM+Jr1giP?`QcBSkFh3Hur`GjsN(+07sXT?h3l@T<|wJM)%KIc@}48) zxjwdlfs4K)H2|N@-=y_SqIN3MiO5n9yORF805X(w+dJlIz73F)s*8D%BdRsmlj`*y z@B#KiZHVSlK@u<(Q9)T}*xzkA@7Yt<HzWeU1<27IhLC%ipYdyAzS;P>Av(smk4pXB z1#rcU^y9@B7S=7jI-c)V=3N9Usr_vuAqx6~279sOEdemthmi3ST_!uw^I;<FRY(d? zNuhlKM!JdOiT$qR3Ccr;>;^(10F27#ol@*VO`l!3FXgL!DDn$K9)b|!-xxyqbTWBJ zk$SLn#8>BChvZp_)%74?2BF#k^pD~^KxkU3-vyzSZ*>_x^KOd{v+-^oA&wMjMHRS% zD?@>4>&)*4eco9=0vK1gJkkkh8z{Ax6G1a-U*O#hkjsNAMg2fFE{rT!+X!|V?a}R_ z2c>8SiWUrp5>Qu1ug~l3j_m?CUYGj}wtHkPOgm808DLuzzOIYN!_mMn%m*C{d=OCF zB)ETvaGb)?-44Q*PO>L^P@EV)RBbE+H#`uwq`J*W+|f#vx1+^@fNnC5O{@yiY?vL^ zY7l5tm<V4R&aR-foKtu%8OC93YglZ;Xtcwe6c4s?2U@HpI`d@{(?XzMTXh(%(`z}< zs4vhSSMchT_{ML^7m6D)8L!-Sh0V$-bHSSQ_!1X!gH%p+urNR#?MW7vJ%PW`!JBX@ zx&UEC`@9R$u?fF}Gf`Rs%JQNEY)aE^93*Y5JfVKUfXGdSE}}tSZH*(kJXS*Dk|;EN zvQHcZiKDJb61+l5qiRuBh$HfpQ2mBDqGi$iJW)dtl8odJ5>XRHKt1rxT8(y6ucja2 zmA3Ps*GA1!_8Xo-a27}$wU_o&9SHx0ng?Pcz;57#SlfPR9sKSf8hOA%tiQM&f50mt zz!*?T<F!VkWM5l%n(o2~FKa1lECWPQ|4W&5SS;unmngvi6Ve)~Kj{)k*+qEq=b-3` zC46p!SOhUIQga;-fQt(0{;N^YB}VAKhrxIHcg<!L0GQFFkogD34Vmaah=I7t^-Tyk zGz(I@IdTQ#qigj_mCyu)&%#yRv(|)wY#|-7GxWv<JSL4viQ<{aA*v$*d4U+MgBnJ~ zqcRrGi>_%H7r2Bw&7}4s+Byu@4Hh_sN-$b)2_q|6>|Am*w5#JDYAj`8B=TNaP7%q? zYrtOuaPYEtE=q4*nMc$#D@DH6#?=t&0ui*hxr7KQDumj&E>b1ofM?a9VqvVNN6aR= zJ^;KHsDLv_;~CF2g+TSn2q68i>#I^_B>~H<KM@V|<m%{2YzmY;!B8Xwy#=A4j46x@ zU;uCEa>EP*HrLZ4ePDWc|1jlN3CMw&@B&yslou-wGJJRgEKAkJH%sTgLX(k+=Q>hx zurV_FwPxR%Wb{1oQ1tiRHoy9%@iq<@F{ZvFgT7*|0B4{K(nXG6L<AC!SaU(7{`#;B zC=61LlR&|zurRd_ZlJp$MIrE&;wBqp90q2Q!x0UH4;n2&VVO6j_W=`ZLdDNi6b&dp z7x-78rf>r`4udj=CLfE?M}rGs5qKQDhO+!#nDLQrH&TPv>6!?{^!j${vd9Xu7rI8M z1Eoo&2QoZhfx?Q4C3<gzO|1YeF;Q(e!wznt!^ay~(Y9fr<vR5j=jQf-;3KvA#1Pev z^1lZPJx|(X8E%w@-8bwRduMK~S7<>K56Ua}1A%W=GnY3UU!yvC$JHIwP2a<{uya@U zz$Z_+i9r|eZoDQ_60}sx0ReLXbHJF+kQRZoQ#d0#z>cy&^HWHYc`S{HA=_D@Zllzr zAqWQJ(HZae=??@q>HTi4+!LWA^chrIHEZ2H{pgT0Ry04r;2Pn7sEarwSW#Xj@|T@z z=vuT3>n*GSp*8m5aqOBwlWCH4<O4W+xcw04jnrW_q)cMDrM^x}Z)P;*zmm7p!*DAI z@57|Efp$DM#aj0R2dE5viS~fJ8j!4{VjU5*c-A5@jP_oOoZDKCc=koj)gWuUFhe{| zkke%0?{0nx^suIM!$!?YLLk_=3;J^HlA9z_azei6;xy&iVE)Ls3p-QzghEAFMtB2+ z24Bvu0664u3jsooe=>1X8IHy}g%{#T@svQ&NJez<!Nwv{%b=LS2{W-wOw2;4x0Wg9 zNbP$vPrYFZoMA9w`a)ll>1VfoO*3wW`(yvHeD${+#79AGm~QSRUi61SbdDxZq&1^; znn^)oQzvO6aENVI$ZKHAh*bmZ<~N6s4rOg=Nw-NHQ;?lxT-cn(O!&K*mxp37mHBhE zq0%4rN)XtEE|3!X7tTgjpg~ic_#FrJVdA)4^sk#Cv%0TRrF^Pj5Nx7a%60%ouqG&o zr={X4e84B-O2>3mI2Fcay0%G*NE6OPIzP@#hnD?Kn4-Pzucn2GyM!zA<8c&`i6Ehc zO>y+GNjME8*bW8px9Mb!3ej#TB`hprh<v^qog4j=G(x<i(60_n1V<z!zBUTTOi%TZ zUub(_^k*qT{$qv$pM^TdxQZ9og`3d%ab*ypxgxC-G-W%1#G(j6hvxxcmnNmnzrBd8 z$>C7&Tn7I@>0oe1*wnmci;hHtM-%<GWRvfd+C(g(dXV<CbYD0KEFoSAtdrSFDBf{p zzXHgbgSs+0z#1cYd%n;B98@*53~xd}cupt1kd^LyUJq0wk7Jgmp!@^%L%dCz&J|EL z_c7tNXpkmq^XY~pyx3>8h)dw6uX-8eM`jWg0!@_+**xrE-IiRDr7IcH2&RK@1PI3y zrUW8@JvWAPYa-rmEuM<NZt+_hmlXm*SEH^nlbi7CSQfxSs2{PTF=1|A6Ae8F!Go~Q z6W@lG$VZbevdMBZ&ZnF*cf=xH1SIy+Ob3lVLc$ZErZwHXn#tfIL3a`W<32TpTH!uX z*^@33&}8m|w{TnzVT-g6gl2mBy)EDn5hs=Py^wz7%DGmoQL$VeI-uBM{yp|=?)yP% zhgQWLsf;1?E-)a>$RFwhuT{8V`abi(I2s9rg5sq=umy6=?$#qK!s1u~g@q5%=yg*7 z@FDby7lGsQN>#%lV7%N4>zzmGhF?oCLaOe~UC>eaA<YPy{=LUy_z~sBI*Pa8l~O%x z%PmtOU1%uHf;>PxP>_lc%kXa=sDVD6g?6PP@K|SE(U5$(Mm*X<$6o^v63oa-qFFLI zP1hly3<XoGFO$xJZCSy{MHmLEHSLucvX?h9+nNtFD3KcHW(`YG(LuPm;xr*hlZMtz zKeUsrY6zv-!G%=o!~)Zd5G!2KXYlmd8GSZcqZ$wj6bm<n%gzAXjPmZ7=m+(HC@G1< z!%Jxf<nc+pL|NR40D+2O0j^L3GP;)=AWe6OG&U0ft=F`%aYveP^j1<pZS>B!h*Y4V z!N0fHu(*TmzX91cG7BC;kgV)Yuu{~Z0^v_mlE-@%4J;-4uUNX{dE^==1=}~WHza8< z!V=)H?8=W8dIGD0`fs2INOs5`hhC3#aG$=j3|>dj&^dKPvQVP9kJ85_acia|`UD>| zbp^lw9GO%al4F!L&9pxvU#megijrG|7Qj)26$+F^xSbVWjl5LZVn9tZpX&~blEZr^ zFo=gGMQ@Nwll2lVKMuwW;mwyre*@RhzzbaumzoGPxJp)T;I{gPEn!YtU0ZmX)&qLr zwr0{X$VPZntpE?HAl^7KtsXm}{Uf{@7$L5_wQA;xt)|3LDPDV!nu%c33tD1h>rwtt z7cZk7x@<Z|j$mDnS`XMJ-F--9AZ;Ne_~=-sjlnB~--IvyeDfP0U8HR^cwTRIh9)=c zp9jQ8`?>oF8uNHZLm;KU@NQiaZS%cR2D=;)d~(5L?@q*x0R;_w^|K94ncH)CaMH#1 zUy2R&caWM?Bwi&^O1wb^kMO%I;Ec2f`3=WqWfgD=6ESh9?`D)|IsQuAzNuZRC>5HW z%E#R8JDtG1dEfzw3&jKY@nQYqhERJ@z<NW>H~7+%tTqkd>;6jQ^&RElP-G2~!nC8> zxQO5sJB<KP9lcq)%AyF0HGm+5tm4<&;Ti<aN(0Q+fc~*9#6)q};vcvIc<!JV!S`C# zQ@)=o_P`Ep63UgU7LX~aB_sJJ5&8aPtioYD0fGqfM%x>zz>ZU-A{IA;Yl;1PLm_ z<JD&XY#kTzQeaUZy06z7iMF<hg@uq8HHrbyypRIy=i8fIf*bTf(byFL752Js(6xv& z*64VRzGK8efrd}15rGA2KUeb!%XC=`MJ`{5<<8t|uHA8mI&<(%tWaDX6SC-4&_eB* zSv|>rykguk99cD|q}%|*Z`he|8pmm-3?<MNJC*G5V^uQ^seC#lVQ;IT8Dp(&p@YAz z8jMqPD=odDHdtacg8kkVTwtvcpxO85qGGw9%f5^L3YN}&{e<fU3y=dKs3WA5Fh2wR z)COJm7EBTS<CXvI!hq>SNjZR%J42rCxysSJ5r9UFXH<679?mXkL#zzYv8CRZ_Fo6G zC-6rXi(D&`H9>9FWY>UIRB!^9Svk*T0l+BQpd@0ocZ!iadQ*lVT}}%_TiD}oH|qob zM*!Ly(O3yjAM5bi!|o+VWDYKptfG8&1QM}m6e0y4G}&yjP}>nd2<_HyNZ-KCzfA>^ zu>OHCx>!2^^V`$5ffTz)!Gv`7!0U38#ODWB-=I}`474C>SJTwGKp6CrOdTF@<513^ zhiEs@ru7&^d`Ken5gs!mXV0kcKP3YcLxDZ#3tS)u6hPMeE|XE4uK>c4`#~P+v`pJ~ zxcKbPw}C*@+s~vgNV3>B=?g9WHR(Iz6ixb?X2$o`mBgi!?ZvT?jXMBoX%oC0MW%_| zSmD5^G*yRhAq_yMTGpTiikt;E0AAmXbaG*9%e<^tX6>9z-(cf2LS$*=2YMjY$0Foq zx2-WC`15TZfH<{L+emf<yHRI2XVqlYEC|TS!6<?PXM_KB>MRXL9v((OML6q}W@Dfo z;=rXgofT^im2t<V@f#Lwn@k3#^J;c{gz_1ZB|3LRUc0#^L?HTCbH*^{8j@H28e+72 zgTOKNQAb$NQ&TRHh--(b(P?#xQ5RfC_I1Fs_LUJ^<Wr`qPuXX4>JLPY_M0%2Tl3y| z4f76u%IpDPM<NtQI=C?6(4^#&JOSDbzM%#ILuc2}ia`O!V;hP*AOg7sZ9ZTc=?B`9 z<w^2pdI45l?+7KL1taRUMOMm2)oPGHyJL9(Zw(l*N9N_IfYB;JwptjPR8Pqs8iy;; z8c7FIxEJxRvZjG!RmL<8MAf%TkEcUmH=d1upZoP;UJL5NQN`W~43YHow)Un*soo(9 zr}h-9X`n*>+VS5`I)c5tEwC0m6y(Vdj;tXv8<*%P#dMQ-taE~oSyAEPyE<eJdY-xd zc%z)KdIw9=18hH(+X2S)MG-}ccoH}4(7?O6({wP0_SJM}4AotWK-kch@zC+A-i_N! z+7$+Y=;mN>_>uslBWUcUDW8*HOgfpPGrZJ0_VWTESVd4tCl-*x1Nm4M+G68IQ&8)4 zlIz8HSz?kxb0L1S{sh^7+~FfU`yOj3Hp)#?3siE_cwB^FlFb{;06jj?v%a;#a%x#M z=l&X(@|gv=qHBlzjD}2GgJ*CmBh#Do+Asv$!o7gVG7`MdO-Nu<iuAZzn{sJcJBaTz zj^eG^(V|?Fwo6c$vOXh1MO;5%xP`9WANNhE?8$KoA_VZaK@jk-Ix0x$BDM(<ao1U7 zWbw%of|!Vv2hi12NDh^Pl?Z<siT6Eq#A2pXea3isgfn4CIs=G*))~EXbQ%O8OZqqW z=x7MjiM98{q9+P|%2^uumUlw>d+$0>Xd(Q_`fuJ<bz|Si&2^2Zw5z_fR~2>GBWy{1 z?Oz|w{oj5J6}_<oAd^h@!p+5K>PxByGzrUH`Be>}X+bFayW&mn)W`jd%F}Y%OQpJ_ zeTV!GIZaLH)wbZMZuf>#mDV&B8udzZYI2Pl2Rq)|RA50v_~*yvXsIY|oqo!M;*{NY z^VL|z2Fn3eK@|jfwO1)k0<36eq+<~pwUUhk?jr6n1xIHsQ~@mZI(S{Z39+I0)_08~ zeQ>l8Y6RTVhA4&hHM3^cKrGkHwk@q3L}!}$Z;g|OPT6j%HHi8Gnt=#ojS=pcDhsto z@+!qjLL;&Zhy&l1@I;16blS$2FXdNBr#H0&cSOVM;Jzb*ig>-vvZ_h_B<0+d#@qH7 z173fpy&N`*SKyq0TXDt}>mhyXcbhZk??eRDL#R#)+M6Kjr9ZJsGv{Z9s3XNX>8zk) zJwF8ai8isuA+LZb;Ob$}zv~#m3CUymM;hGmfztDE>B{pQZH`O;ov4oY77#<VUPjPV ztPFf*d+&ur!&nOgu?b?oEvS_kC6ysmZ426WM&w)VJK)g=_5-nwYD*nA`<%f~ol>Q| zA2l5kf)j$&Vx)jPK5WDe5N=Op4-GzL5SJu4jfc)9v1bX-PS@E4BnCK!C}!bHOnY-{ z>6u||Op|;SgiP0(VZHO8D|sF5qd&Jc6_L~h*1tM;vG(Yh1vu>jpRRc&&z9zy8ua6x z-l^p!F??uV3h+qzd5ErPH?-~0H<aF2>>liL!-uK8E1k91Od$O_Ws`q}?(b=Cue~Xt z_^Z9;knVPW^f!_x6@t&*e|=?$f(oP}7>48_{Hr~NZs<GQ8gP<k@m(|G;}W^)l(ys> z;t>X*ti1NM)ZEC9^|Ramwfj%Azw<wKw3DAFfSjvqIuC**0bDd}TAJBs=-DHPfLQJj zVh)%BZSxUsjXY;8+-gt~q!J-)?XMwt@fDkYhS&ovAqSd*jw>Kn&XzycMgjOYw~o_~ zA7GDO=Yut*5>PvtIZR->cr+bsgUzW6FI}H<;ytZt;7&cad0?B-(JR_9jM~fuXTb>^ zXm>jJq^-Us5GqpzODW646OC0meiD`hnGh9Grzg0v*!~)=e%}kqA5KP{Y{__y(91$Z zqt}I#h(r`GKycnR5`Ro%Z@5$EueN3p_RcxZ3b4I1r{k|HkW9@2rp|5hBMH26+f%or zk>SdRa(N?LETK*bUJ;Gc2|3fq6!~TAA?NjS>V_P_H@$;zIwuO};8$WPQDeZyB1oRL zq(~b<aOGJBv@#Y^7^`hDIMeIkZPM}T_lXQLFX8aHepHV~Ia@>btXjDN9f?V%^PHLn z$f0WG7%6~p1P$z16eW3O?Mf~t?FaTw9YLZ1G$(B8YgB|oC}7;e8|K>r&WMF(^(cQ0 z2MN#6!MHXwaNAVs)HX8yvXFg{B*uCUxpGN+e>xX}8zOpkw8@Wo)&UI=BcZ5udapzH zunHO)51Xb`6J7=1B9*9*Pq~cc(Hs-VkK(F5Rz|OSsxl^QP>L!><VM0?G*J%12h&bq z$~K@3JomSlgM(?!#y!&(Q?n71nYQTpKQ$?Yx2d5466*RM9U0BYN^HMMWVS`IpXs&^ z+-gochYpf&EIRMiD9T9z;0x%D7<GmRm8l6W(w4T^%ia-<aglg&mE|geMhDoW5K&(! zc8qbcj*v909%V)c-8i;KSG@EG9pMs;cWpi`*cZSIZq4_xBrY2W6bv@Q)-ezAMy0^3 zKQGrI(4s@Qr|9Lx21^pedYe}p?#K_lye3`~q%U+73YX>IP=Y$)sl~Lt1z}@{JAjF{ zMo9uwG1d2>on^;GeVPx!e|5;>bW{M|OdoEc$F*06PBeX9px`l3PC*BvwVi<6LmzC@ zM=)xn*8rGaGj<c{fOh7JA2je|QV-)3u#@OBLsXNvgRvNRr$K&atx?-+Qj889*a52; z@TNM1tKB^srRqpHis(?@@aJsu5-QfI6QLJpS+PTOCmQ`g2gz9_WHcn&8Vj8ct*QCr z>6$h^tV2y}!=I=fFOQh?i3XaU#leO{2k_T!4r@S34}8(4@K0GY!H1zM_`IijGZ8!4 z7t@)*i9cnTr=gK&kU+vMOwDQ$VApHk5kRNdzoU~M1+A)GY9N*jrBI~sCq8@rIzb8R z>5235x;H)y8%6qGJmKf}I)Q@+>%IN`{od&aJ>7k>YWr`5OWK?G6XC@hMrw#~^IMQY zdk~v%8!ex8z@KtPQ-|+R>~L2T`VrJvtHN$qgkLn&+Zw&-1cB+aK-8`!NDr`rcKW;m znkHhhH5x!kxc0Z>nBRv_lK=CA75bn7c&w(I(x3f}%&LD|`h*WWI6J%c56A8P;k4bK z^Rhv9^ce?Ir=7~#`<e|k5)wDRpb8MM=s<s3$iBIG6)<G<3c-We2(_1u-J|kI)I@7) zgL)-A3<bG?S7c3n!K6&}X#qet>$8%&X+x~~apROdNIK{O^&T*N8KHyMbkwCEHv%|T zhR!6{D~B&p-!WH1h`4sI@?k497?n4IG(b(YfxQpZ<Sn(1z;&k=ioC%e6=Ig)JIhO` z0EeWkbcAI_SfG3ivB-Pr!xFpc<CH-kOrp9w^4jTO0J0lr`3!SJWeK1xsc`U2Vz;9+ z=@6riw-PdhE(Uk%lw>QOdpM<br+shepcXVBLRS0I!pDg0j_(+G@b9Avdb{#Ac&B!O zxi{{Fb_~)Dys1M4p{~9Ulf(g`-S}L{SX@w_HC6u(&5khY^lu-_wvI6t46FvWq)FD% zb$!YXa(pjf<@v&LI&Kq&NsQ<)U)1qwZL4*759Eh!bEW^gHp2a-K}vB^+whpS4*h<u zMiJ;}PSCJ7_XTwH_i3F^|DLx$2{0S<8ndK!8ZBlw@WXg1B$E^Ytur;x#<J+M%?s6! z9nsDW>4&BuZAqN_cE;+UKC%WGSEqe~&Oq}&qNQt}*MsXKhx(Y580m8mps9|r7VNr$ z*mVG*>I8L6+Hl4NILdKC9%ym6V}BnN8(k)8$_IY?fHCy(uxB?)dE`*0j#KdzigL;~ zWk;+@LY_Lza0=yZZ#a;BPudxVqT|-5X;8^ahtzc{1G`FoA#@_NV$`Yf2pzST<D;Xw zS93&+Pt|TUdPLKmVcOu(jxau^=tP{4RZbUz(ilHbM27DNWl1tVWlCD*20A7UEn#DA z`>d_+A~_(BTC!qbX5I3XN&6k1w!~~tCy}qicKYC~+#kLK5gbs&2N|zQ9vWi)i?06n z@9yspRNfqYfa;^mX;4je27zE95&y2eh%QI25j7Q+^zjPqSEudtc?-H2@Wovq6?(4U zDW~`^a2u`18wHjDhBj@IYTx{8Zz{XK4-vl+f>-u+)@DQYvCN+j5V4#+<Cc>nZ?JRJ zkP-WLqYe<f)1N`dJ+4HmjJQ7bFAg9x0Ls;A2qIKF;)Qq_{!A`j$HEOhqWxyjxfXqB zX+~FYPTg|}eVQB3gmmjb$$l$9e+u>>ZptfaU|e*h1_%mt4HxXQKPz{OLm3@?(C#MK z=-a)?TozZX&*5n^sZv3+>bP8@+1>{|kuMt&gBjoWJ+AA$!#HqHR>@dG2F&#L5dX*D z-ERu?O&>CqA*6GvJ}v`5BjrJ#5Dp@hu6)D}hJ1$@Fdhr}^PQt`K}TF0zD{oI_nA7x z2_)wJFC4e(GY(gM1^@s7glR)VP)S2WAaHVTW@&6?004NLeUUv#!$2IxUsJ^*6$d+r z6wFYaEQpFYY88r5A=C=3I+$Gg1x*@~6c<Oqwcy~#V%5RLSyu;FK@j`^adLE0bdeJO zmlRsWcyQd0clRE5?*O4uVVc!74rsb<rjrRVn_CroULhcg5aJk?nPtpLQVPD~>mC8V z-o<&A|G7U$pPIKA5D<xHnPJ+*8^qI_w!wLyIKoP@N_<W{X3_<TAGxl0{KmQHvcNMV zW+pvP93d8q9jtUPE14Sc6md+|bjla99;=+UIBS&}Yu%H-Fr3#{mbp$diX;}X1PLM( z)KEqRHuY78PKt#z?I(QvL#|&UmqM;G7&#VDg$CL6ga5(rZmq)PgqIXf0NpQ+^DzSS z>;lcY<9r`GPV)o^J_A>J+h1(}GoPf_+gj`h=-&n|uG^Zj2VCv|gHMKR%B~coDHIC8 z`x$*x4j8xvde^+(TKhPC05a57>IOJC1V)RLz3%hwP-k!do@w>>1BUx@uVtI$UjP6D z08mU+MF0Q*0002F7Z>{S$p8QVsQ>^3Ul{-Z01*HH000004g&{c8UO$QoX~=%000<= z2B`)H2V@&FkSZ981ONa40001CwK%2%0=^Ow_o=CE!b$33VZI?Dss#n{lao548oC1m zFOVdw1qI11EdX2^Du*mpuRU$PQH|1V?T3fU3=GssNsrQS2VoY@w|ri)O3Ac(v;+j| z+MVHmfE^th;Hqe)rKO^xqNAgv@$&KP?d{&4Xy@nWCMG7Rrm6Yv;NRch`||4l|Nrgo z@7~Fb0yePw^z9^^0icUi+S=M4l>+zY*x{~i>gwwJ_w!t2bzER+)z#JJ008Of>i6l{ znVOsW`ugJH<KL)b_T<zxuK@D$^7P%w;jL>}SXuq`(Ea@UDWCxQ@Z#{(yGCt8M7#)A zo>J!J=I#Lj`S8#7^!6;K0QKO{;G}YR)j#m>@c#JM{`=))#xQEkGNz=a-J4~psi|tV zPWtla_V)Jj;<N6}vHtt`@z}rJqFMg>-um+CT&zd==-%Ygo1~+rvhIbbxTZR@1c}~K zmgij2=gQjy1M=C#&(F{K@8$LB!ppXRfPjWEssPx?p7-n9lh<xBt^ml`%Bt;o%mD#c zTV&G!08mw5&%BA$!;wO?4Pc>P@%Hh`>bchZvBCiX|FE#1pPz%!T-(j2Prw!P;?45k z!mh=#xa*jqx}-~!F`eXZcDi4y*qd^7guU3d-Tc3%l3v@CTI<-I{djoc&5|W0C9Rri zyrg5tuynzwYxwH9{$^(C+Nk=9is;9trQL`#nH#svw?;`(P@+TMx`?j1v!d>4jLLJ$ z`Jl+i$p4d*_$DUtGc(@Mtm_aEx3}H-^4V!^c)GTbf7(Wqo03|^BG;Z&=+m}>#b&kJ zoqvUvb#<e*6cm+}m6n#4DUKV=%&pJEoRGL$W~FH6#FyLL#>D@Mq@>-Bj@Qb{;;s-7 z-I-#ls;d9V$^V_5`RAAVJUq0bb?9|<??pxB+s1E$GM1H;rKRqxtN$o3JsiJ2KmY&$ z0d!JMQvg8b*k%9#00Cl4M??UK1szBL000SaNLh0L01FcU01FcV0GgZ_00007bV*G` z2jl`A3NSgW+H+n200zTJL_t(o!`+zuPZM_>$L}A&5ll?fM5jyKW3nuR_Igy>K~~b% z0;NwBT2o=HrZ7kVp$%e?pfH}25)c6sok|H0f>Mnk`^kWURIM{E_4#3C>U8SHmSxY| z_xruOUfVn9l~xz`hV+ilmGA5O{@&;N`CN;o^A{GgqJbr|{yWMDIgZEs@|@{?zHpjF zPagI0ch=6L`JH0&f+zm>j73ZHL0U+dHERK<kIj*sp<&xYmeB=hp;KTDEJ)*+{pK$t zXQS8|v|uzX)GphZ%Ee}FjqD7tT$c23TIkZUo#|D67K;UAIpbgfnx{Q{pOHTSn8jjs zhC~1hz>-7OAl#XrixqjF0cZ1J_iHmy1ZNh1nq?WATA*?9$oJ&sm8)>!X)mxgvYhcE z#L3wUt@_K;E9K1Vr?kLhI<Y$Q`8`MrK0$2WdBNy1>J6nua)x0#mynSTMTCu2jihio zg5x;6jyG0;2vf7Yz96a5c<#n~zI*p(xvdDLqJYFg6cIT@Um$@KDxycSSVSTl9#}>o zXAsOoPCm!^yzf8`w=-N*t|H1s1vZC6Z$m>jica1=+|tr=`0&XdQ6qYfh#)j7LS$>1 zn2-V(w>&veeY4KzvpG50oX&8u$|}|%o}&Q4s)&T|FC95`>d2)d6GRnykFDpVRii5J zIeY9_f^Jb5S=BR~Umr+6kj?E(EH)UxNCcR80S+T+#GMNl_V3vPelHTJ{^2bbyJs}2 zo`mDw-6w7=4*M7?9m4t+b7qiA4ai&p4Li~NIneR_Wi=%1Mon4S;7re*%8H7b8DVs= zFlSOZMJo@$%F3=cH#b)n@1|h8i;Ig_)fDINTR&>UV{#@{%SjwT!)k6;R903DHSMEd zjXmQf`T6;sCFe%rgko_fS5q_%4cpyx@ZiCweVru~OkndQWp3M+d2URj5k-bQUBa2X z9nt{KO2f`)f}z3Sit|*0h~%Cmh2q;WtHHVyjFkK7?BdSU?SKZ&87NrBA%!9{GgF~Z z9Lk_z?P||>(ioi560mGizNi0WXS+F_3B>JQuNU#)Krm7;cIZ>2??!FZJ_D1P8I^*y z7CRP)8K8Xbzy7}8_b0*aOyvN@0|=BO2Z9O3<pik=x`0W=VzEXehLSFx`e?kZ&+fbL zKbz%#KGEUT3)PeaRM{1zQK||m!~y{hCDbhq1B>Z%AIR1%{<$VPEeVUaqJqWh%pm8K z1N+Yv;_UQ~sX#kOoraNJc`4=;N_o4e2rV;+xSVb%zc}jWuN|iPfRsC6B+s5@mmz_Z zJQzu_??kffwA*DVm{aKW&Uihpd24h}R~VM6?P!p6)XQX9SvzH^<j_o~wx^I0iKI(X zXqP>ixI3JJ`D}WNUZ3vO%^RY0ma1$p8kLBbMZ(Myrc+DI2qwEdt?ZI?Jb|5h7v>{B z^H|h=zfdTsu1<5|UY^dB%2JXB){cTPotlFEezE&Om*h6=)Q@gqKKgqL5s{Hm4kcaS z>&$2>Ej22&4U+nL3dVG5$rc#71v~YF+DEWcKh6ul7!@q=bynJDG9hyv0a&0@7m~15 zflmDpBnTVk(5ZPkGZodfA#)NEwnf5pY7&-mo9@&PJ1Cgi8!V5{4_^5=Yb$~@U`|WH z3Ykt#!pyR{anz}=b#>VxOmFbB%axOlv$|S9gXWAhjOo;rGke|KIOx>ZjKf9*)95%@ zH~2U!ss%K(lr)U#)P-a=+G~sE{z$quGBPp@VIq&44HL@wIIEqWp00(!HY!0rVmh^q z+|;tVx{=3orr}|uG7G@GPL86#z$Y6UvOw_w0_7+R!Sb~BLJ3l#U5m<9o=L0Kc6EUl zt<nTif1ZQoRP)Hj)2W{Ylu8w1^A<|Mh1hJSOEYITbMe_{acZ7UPdq+#^Mv{3FVUGN z{RKI5ySbl+m+9<7^6)nIRjZYV{hN~a=#yUu*1^N-hShO?24R$N>KJ*_fAzO1KwCF3 z@DoELh`8K>_sPRsd(NKih9`-=xw%)*Oif+B{PVy71KUf)!P%PqZz9(5!NI{zD6*nI zH+TCllarHEm)~GupX`m@+p4n4>wvbV@@7TF1}X+<+qZvp=FB9+0<h)LLY%E18Y&*z zw5Av|$JYYE4{{OhT`U9^2X89@rm3m3vy)+A@3bP?+Bm?%vT<c+2~-g4_5OZDyRtU+ zWn_8V23ln>tGBnmwH4xmXv<9y);?RQPz15CZ;l@A?cKN$<mA;WF{eITNgIOyN(8hO zASZisTN7j6)DdfA;twm(4mg^Ky;@4f{;y)3{R=&p3b+WMx>5iD002ovPDHLkV1l(> Ba*zN3 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js new file mode 100644 index 0000000000000..51ba2a258faf1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -0,0 +1,75 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'Magento_MediaGalleryUi/js/action/deleteImages', + 'mage/translate' +], function ($, _, getDetails, deleteImages, $t) { + 'use strict'; + + return { + + /** + * Get information about image use + * + * @param {Array} recordsIds + * @param {String} imageDetailsUrl + * @param {String} deleteImageUrl + */ + deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { + var imagesCount = Object.keys(recordsIds).length, + confirmationContent = $t('%1 Are you sure you want to delete "%2" image%3?') + .replace('%2', Object.keys(recordsIds).length).replace('%3', imagesCount > 1 ? 's' : ''), + deferred = $.Deferred(); + + getDetails(imageDetailsUrl, recordsIds) + .then(function (imageDetails) { + confirmationContent = confirmationContent.replace( + '%1', + this.getRecordRelatedContentMessage(imageDetails) + ); + }.bind(this)).fail(function () { + confirmationContent = confirmationContent.replace('%1', ''); + }).always(function () { + deleteImages(recordsIds, deleteImageUrl, confirmationContent).then(function (status) { + deferred.resolve(status); + }).fail(function (error) { + deferred.reject(error); + }); + }); + + return deferred.promise(); + }, + + /** + * Get information about image use + * + * @param {Object|String} images + * @return {String} + */ + getRecordRelatedContentMessage: function (images) { + var usedInMessage = $t('The selected assets are used in the content of the following entities: '), + usedIn = []; + + $.each(images, function (key, image) { + $.each(image.details, function (sectionIndex, section) { + if (section.title === 'Used In' && _.isObject(section) && !_.isEmpty(section.value)) { + $.each(section.value, function (entityTypeIndex, entityTypeData) { + usedIn.push(entityTypeData.name + '(' + entityTypeData.number + ')'); + }); + } + }); + }); + + if (_.isEmpty(usedIn)) { + return ''; + } + + return usedInMessage + usedIn.join(', ') + '.'; + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js new file mode 100644 index 0000000000000..c8ddeaf3d3929 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'mage/url', + 'Magento_MediaGalleryUi/js/grid/messages', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function ($, _, urlBuilder, messages, confirmation, $t) { + 'use strict'; + + return function (ids, deleteUrl, confirmationContent) { + var deferred = $.Deferred(), + title = $t('Delete assets'), + cancelText = $t('Cancel'), + deleteImageText = $t('Delete'); + + /** + * Send deletion request with redords ids + * + * @param {Array} recordIds + * @param {String} serviceUrl + */ + function sendRequest(recordIds, serviceUrl) { + + $.ajax({ + type: 'POST', + url: serviceUrl, + dataType: 'json', + showLoader: true, + data: { + 'form_key': window.FORM_KEY, + 'ids': recordIds + }, + context: this, + + /** + * Success handler for deleting image + * + * @param {Object} response + */ + success: function (response) { + var message = !_.isUndefined(response.message) ? response.message : null; + + if (!response.success) { + message = message || $t('There was an error on attempt to delete the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + + deferred.reject(message); + } + + message = message || $t('You have successfully removed the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: true, + message: message, + code: 'success' + }); + deferred.resolve(message); + }, + + /** + * Error handler for deleting image + * + * @param {Object} response + */ + error: function (response) { + var message; + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('There was an error on attempt to delete the image.'); + } else { + message = response.responseJSON.message; + } + + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + deferred.reject(message); + } + }); + } + + confirmation({ + title: title, + modalClass: 'media-gallery-delete-image-action', + content: confirmationContent, + buttons: [ + { + text: cancelText, + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + deferred.resolve({ + status: 'canceled' + }); + } + }, + { + text: deleteImageText, + class: 'action-primary action-accept', + + /** + * Delete Image and close modal + */ + click: function () { + sendRequest(ids, deleteUrl); + this.closeModal(); + } + } + ] + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js new file mode 100644 index 0000000000000..ec750afff29bf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (imageDetailsUrl, imageIds) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'GET', + url: imageDetailsUrl, + dataType: 'json', + showLoader: true, + data: { + 'ids': imageIds + }, + context: this, + + /** + * Resolve with image details if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.imageDetails); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not retrieve image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js new file mode 100644 index 0000000000000..4d1120badeca0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js @@ -0,0 +1,56 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (saveImageDetailsUrl, data) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: saveImageDetailsUrl, + dataType: 'json', + showLoader: true, + data: data, + + /** + * Resolve with image details if success, reject with response message otherwise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not save image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js new file mode 100644 index 0000000000000..f6dd277fb85f5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'jquery' +], function (Element, $) { + 'use strict'; + + return Element.extend({ + defaults: { + containerSelector: '.media-gallery-container', + masonryComponentPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns', + modules: { + masonry: '${ $.masonryComponentPath }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super(); + + $(this.containerSelector).applyBindings(); + + return this; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js new file mode 100644 index 0000000000000..cc4d759069c67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js @@ -0,0 +1,61 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (createFolderUrl, paths) { + var deferred = $.Deferred(), + message, + data = { + paths: paths + }; + + $.ajax({ + type: 'POST', + url: createFolderUrl, + dataType: 'json', + showLoader: true, + data: data, + context: this, + + /** + * Resolve if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not create the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js new file mode 100644 index 0000000000000..06277481e1142 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (deleteFolderUrl, path) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: deleteFolderUrl, + dataType: 'json', + showLoader: true, + data: { + path: path + }, + context: this, + + /** + * Resolve if delete folder success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not delete the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js new file mode 100644 index 0000000000000..d7f756d8bbd90 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -0,0 +1,186 @@ +/** + * Copyright © Magento, Inc. All rights reserved.g + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/modal/alert', + 'underscore', + 'Magento_Ui/js/modal/prompt', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'Magento_MediaGalleryUi/js/directory/actions/deleteDirectory', + 'mage/translate', + 'validation' +], function ($, Component, confirm, uiAlert, _, prompt, createDirectory, deleteDirectory, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + directoryTreeSelector: '#media-gallery-directory-tree', + deleteButtonSelector: '#delete_folder', + createFolderButtonSelector: '#create_folder', + messageDelay: 5, + messagesName: 'media_gallery_listing.media_gallery_listing.messages', + modules: { + directoryTree: '${ $.parentName }.media_gallery_directories', + messages: '${ $.messagesName }' + } + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['selectedFolder']); + this.initEvents(); + + return this; + }, + + /** + * Initialize directories events + */ + initEvents: function () { + $(this.deleteButtonSelector).on('delete_folder', function () { + this.getConfirmationPopupDeleteFolder(); + }.bind(this)); + + $(this.createFolderButtonSelector).on('create_folder', function () { + this.getPrompt({ + title: $t('New Folder Name:'), + content: '', + actions: { + /** + * Confirm action + */ + confirm: function (folderName) { + createDirectory( + this.directoryTree().createDirectoryUrl, + [this.getNewFolderPath(folderName)] + ).then(function () { + this.directoryTree().reloadJsTree().then(function () { + $(this.directoryTree().directoryTreeSelector).on('loaded.jstree', function () { + this.directoryTree().locateNode(this.getNewFolderPath(folderName)); + }.bind(this)); + }.bind(this)); + + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + }, + buttons: [{ + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + } + }, { + text: $t('Confirm'), + class: 'action-primary action-accept' + }] + }); + }.bind(this)); + }, + + /** + * Return configured path for folder creation. + * + * @param {String} folderName + * @returns {String} + */ + getNewFolderPath: function (folderName) { + var selectedFolder = _.isUndefined(this.selectedFolder()) || + _.isNull(this.selectedFolder()) ? '/' : this.selectedFolder(), + folderToCreate = selectedFolder !== '/' ? selectedFolder + '/' + folderName : folderName; + + return folderToCreate; + }, + + /** + * Return configured prompt with input field + */ + getPrompt: function (data) { + prompt({ + title: $t(data.title), + content: $t(data.content), + modalClass: 'media-gallery-folder-prompt', + validation: true, + validationRules: ['required-entry', 'validate-alphanum'], + attributesField: { + name: 'folder_name', + 'data-validate': '{required:true, validate-alphanum}', + maxlength: '128' + }, + attributesForm: { + novalidate: 'novalidate', + action: '' + }, + context: this, + actions: data.actions, + buttons: data.buttons + }); + }, + + /** + * Confirmation popup for delete folder action. + */ + getConfirmationPopupDeleteFolder: function () { + confirm({ + title: $t('Are you sure you want to delete this folder?'), + modalClass: 'delete-folder-confirmation-popup', + content: $t('The following folder is going to be deleted: %1') + .replace('%1', this.selectedFolder()), + actions: { + + /** + * Delete folder on button click + */ + confirm: function () { + deleteDirectory( + this.directoryTree().deleteDirectoryUrl, + this.selectedFolder() + ).then(function () { + this.directoryTree().removeNode(); + this.directoryTree().selectStorageRoot(); + $(window).trigger('folderDeleted.enhancedMediaGallery'); + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + } + }); + }, + + /** + * Set inactive all nodes, adds disable state to Delete Folder Button + */ + setInActive: function () { + this.selectedFolder(null); + $(this.deleteButtonSelector).attr('disabled', true).addClass('disabled'); + }, + + /** + * Set active node, remove disable state from Delete Forlder button + * + * @param {String} folderId + */ + setActive: function (folderId) { + this.selectedFolder(folderId); + $(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled'); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js new file mode 100644 index 0000000000000..decc337e1b83c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -0,0 +1,477 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global Base64 */ +define([ + 'jquery', + 'uiComponent', + 'uiLayout', + 'underscore', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'jquery/jstree/jquery.jstree', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, Component, layout, _, createDirectory) { + 'use strict'; + + return Component.extend({ + defaults: { + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + directoryTreeSelector: '#media-gallery-directory-tree', + getDirectoryTreeUrl: 'media_gallery/directories/gettree', + jsTreeReloaded: null, + modules: { + directories: '${ $.name }_directories', + filterChips: '${ $.filterChipsProvider }' + }, + listens: { + '${ $.provider }:params.filters.path': 'clearFiltersHandle' + }, + viewConfig: [{ + component: 'Magento_MediaGalleryUi/js/directory/directories', + name: '${ $.name }_directories' + }] + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['activeNode']).initView(); + + $.async( + this.directoryTreeSelector, + this, + function () { + this.renderDirectoryTree().then(function () { + this.initEvents(); + }.bind(this)); + }.bind(this)); + + return this; + }, + + /** + * Render directory tree component. + */ + renderDirectoryTree: function () { + + return this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isFolderCreated) { + if (isFolderCreated) { + this.getJsonTree().then(function (newData) { + this.createTree(newData); + }.bind(this)); + } else { + this.createTree(data); + } + }.bind(this)); + }.bind(this)); + }, + + /** + * Set jstree reloaded + * + * @param {Boolean} value + */ + setJsTreeReloaded: function (value) { + this.jsTreeReloaded = value; + }, + + /** + * Create folder by provided current_tree_path param + * + * @param {Array} directories + */ + createFolderIfNotExists: function (directories) { + var isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath = isMediaBrowser ? window.MediabrowserUtility.pathId : null, + deferred = $.Deferred(), + decodedPath, + pathArray; + + if (currentTreePath) { + decodedPath = Base64.idDecode(currentTreePath); + + if (!this.isDirectoryExist(directories[0], decodedPath)) { + pathArray = this.convertPathToPathsArray(decodedPath); + + $.each(pathArray, function (i, val) { + if (this.isDirectoryExist(directories[0], val)) { + pathArray.splice(i, 1); + } + }.bind(this)); + + createDirectory( + this.createDirectoryUrl, + pathArray + ).then(function () { + deferred.resolve(true); + }); + } else { + deferred.resolve(false); + } + } else { + deferred.resolve(false); + } + + return deferred.promise(); + }, + + /** + * Verify if directory exists in array + * + * @param {Array} directories + * @param {String} directoryId + */ + isDirectoryExist: function (directories, directoryId) { + var found = false; + + /** + * Recursive search in array + * + * @param {Array} data + * @param {String} id + */ + function recurse(data, id) { + var i; + + for (i = 0; i < data.length; i++) { + if (data[i].attr.id === id) { + found = data[i]; + break; + } else if (data[i].children && data[i].children.length) { + recurse(data[i].children, id); + } + } + } + + recurse(directories, directoryId); + + return found; + }, + + /** + * Convert path string to path array e.g 'path1/path2' -> ['path1', 'path1/path2'] + * + * @param {String} path + */ + convertPathToPathsArray: function (path) { + var pathsArray = [], + pathString = '', + paths = path.split('/'); + + $.each(paths, function (i, val) { + pathString += i >= 1 ? val : val + '/'; + pathsArray.push(i >= 1 ? pathString : val); + }); + + return pathsArray; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Wait for condition then call provided callback + */ + waitForCondition: function (condition, callback) { + if (condition()) { + setTimeout(function () { + this.waitForCondition(condition, callback); + }.bind(this), 100); + } else { + callback(); + } + }, + + /** + * Remove ability to multiple select on nodes + */ + overrideMultiselectBehavior: function () { + $.jstree.defaults.ui['select_range_modifier'] = false; + $.jstree.defaults.ui['select_multiple_modifier'] = false; + }, + + /** + * Handle jstree events + */ + initEvents: function () { + this.firejsTreeEvents(); + this.overrideMultiselectBehavior(); + + $(window).on('reload.MediaGallery', function () { + this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isCreated) { + if (isCreated) { + this.renderDirectoryTree().then(function () { + this.setJsTreeReloaded(true); + this.firejsTreeEvents(); + }.bind(this)); + } else { + this.checkChipFiltersState(); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + /** + * Fire event for jstree component + */ + firejsTreeEvents: function () { + $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { + var path = $(data.rslt.obj).data('path'); + + this.setActiveNodeFilter(path); + this.setJsTreeReloaded(false); + }.bind(this)); + + $(this.directoryTreeSelector).on('loaded.jstree', function () { + this.checkChipFiltersState(); + }.bind(this)); + + }, + + /** + * Verify directory filter on init event, select folder per directory filter state + */ + checkChipFiltersState: function () { + var currentFilterPath = this.filterChips().filters.path, + isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath; + + currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : + Base64.idDecode(window.MediabrowserUtility.pathId); + + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } + }, + + /** + * Verify if directory exists in folder tree + * + * @param {String} path + */ + folderExistsInTree: function (path) { + if (!_.isUndefined(path)) { + return $('#' + path.replace(/\//g, '\\/')).length === 1; + } + + return false; + }, + + /** + * Check if need to select directory by filters state + * + * @param {String} currentFilterPath + */ + isFiltersApplied: function (currentFilterPath) { + return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' && + currentFilterPath !== 'wysiwyg' && currentFilterPath !== 'catalog/category'; + }, + + /** + * Locate and higlight node in jstree by path id. + * + * @param {String} path + */ + locateNode: function (path) { + var selectedId = $(this.directoryTreeSelector).jstree('get_selected').attr('id'); + + if (path === selectedId) { + return; + } + path = path.replace(/\//g, '\\/'); + $(this.directoryTreeSelector).jstree('open_node', '#' + path); + $(this.directoryTreeSelector).jstree('select_node', '#' + path, true); + + }, + + /** + * Listener to clear filters event + */ + clearFiltersHandle: function () { + if (_.isUndefined(this.filterChips().filters.path)) { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); + } + }, + + /** + * Set active node filter, or deselect if the same node clicked + * + * @param {String} nodePath + */ + setActiveNodeFilter: function (nodePath) { + + if (this.activeNode() === nodePath && !this.jsTreeReloaded) { + this.selectStorageRoot(); + } else { + this.selectFolder(nodePath); + } + }, + + /** + * Remove folders selection -> select storage root + */ + selectStorageRoot: function () { + var filters = {}, + applied = this.filterChips().get('applied'); + + $(this.directoryTreeSelector).jstree('deselect_all'); + + filters = $.extend(true, filters, applied); + delete filters.path; + this.filterChips().set('applied', filters); + this.activeNode(null); + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setInActive(); + }.bind(this) + ); + + }, + + /** + * Set selected folder + * + * @param {String} path + */ + selectFolder: function (path) { + this.activeNode(path); + + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setActive(path); + }.bind(this) + ); + + this.applyFilter(path); + }, + + /** + * Remove active node from directory tree, and select next + */ + removeNode: function () { + $(this.directoryTreeSelector).jstree('remove'); + }, + + /** + * Apply folder filter by path + * + * @param {String} path + */ + applyFilter: function (path) { + var filters = {}, + applied = this.filterChips().get('applied'); + + filters = $.extend(true, filters, applied); + filters.path = path; + this.filterChips().set('applied', filters); + + }, + + /** + * Reload jstree and update jstree events + */ + reloadJsTree: function () { + var deferred = $.Deferred(); + + this.getJsonTree().then(function (data) { + this.createTree(data); + this.setJsTreeReloaded(true); + this.initEvents(); + deferred.resolve(); + }.bind(this)); + + return deferred.promise(); + }, + + /** + * Get json data for jstree + */ + getJsonTree: function () { + var deferred = $.Deferred(); + + $.ajax({ + url: this.getDirectoryTreeUrl, + type: 'GET', + dataType: 'json', + + /** + * Success handler for request + * + * @param {Object} data + */ + success: function (data) { + deferred.resolve(data); + }, + + /** + * Error handler for request + * + * @param {Object} jqXHR + * @param {String} textStatus + */ + error: function (jqXHR, textStatus) { + deferred.reject(); + throw textStatus; + } + }); + + return deferred.promise(); + }, + + /** + * Initialize directory tree + * + * @param {Array} data + */ + createTree: function (data) { + $(this.directoryTreeSelector).jstree({ + plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], + vcheckbox: { + 'two_state': true, + 'real_checkboxes': true + }, + 'json_data': { + data: data + }, + hotkeys: { + space: this._changeState, + 'return': this._changeState + }, + types: { + 'types': { + 'disabled': { + 'check_node': true, + 'uncheck_node': true + } + } + } + }); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js new file mode 100644 index 0000000000000..e7c05573a4f11 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -0,0 +1,288 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/grid/columns/column', + 'uiLayout', + 'underscore' +], function ($, Column, layout, _) { + 'use strict'; + + return Column.extend({ + defaults: { + bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image', + deleteImageUrl: 'media_gallery/image/delete', + addSelectedBtnSelector: '#add_selected', + deleteSelectedBtnSelector: '#delete_selected', + selected: null, + fields: { + id: 'id', + url: 'url', + alt: 'name' + }, + modules: { + actions: '${ $.name }_actions', + provider: '${ $.provider }', + messages: '${ $.messagesName }', + massaction: '${ $.massactionComponentName }' + }, + imports: { + activeDirectory: '${ $.mediaGalleryDirectoryComponent }:activeNode' + }, + listens: { + activeDirectory: 'selectDirectoryHandle', + '${ $.massactionComponentName }:massActionMode': 'updateSelected' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', + name: '${ $.name }_actions', + imageModelName: '${ $.name }' + } + ] + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initView(); + $(window).on('fileDeleted.enhancedMediaGallery', this.reloadMediaGrid.bind(this)); + $(window).on('reload.MediaGallery', this.reloadGrid.bind(this)); + + return this; + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'selected' + ]); + + return this; + }, + + /** + * Is massaction mode active. + */ + isMassActionMode: function () { + return this.massaction().massActionMode(); + }, + + /** + * Returns url to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getUrl: function (record) { + return record[this.fields.url]; + }, + + /** + * Returns id to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Number} + */ + getId: function (record) { + return record[this.fields.id]; + }, + + /** + * Update selected items per massaction mode. + */ + updateSelected: function () { + this.selected({}); + }, + + /** + * Returns name to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getImageAlt: function (record) { + return record[this.fields.alt]; + }, + + /** + * Check if the record is currently selected + * + * @param {Object} record - Data to be preprocessed. + * @returns {Boolean} + */ + isSelected: function (record) { + if (_.isNull(this.selected())) { + return false; + } + + if (this.massaction().massActionMode()) { + return this.selected()[record.id]; + } + + return this.getId(this.selected()) === this.getId(record); + }, + + /** + * Click on image + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnImage: function (record, collapsibleOpened) { + if (!collapsibleOpened) { + this.select(record); + } + }, + + /** + * Click on three-dots + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnThreeDots: function (record, collapsibleOpened) { + if (!this.isSelected(record) || collapsibleOpened) { + this.select(record); + } + }, + + /** + * Handle checkbox click. + */ + checkboxClick: function (record) { + var items = this.selected(); + + if (this.selected()[record.id]) { + delete items[record.id]; + this.selected(items); + } else { + items[record.id] = record.id; + this.selected(items); + } + + return true; + }, + + /** + * Set the record as selected + */ + select: function (record) { + if (this.massaction().massActionMode()) { + return this.checkboxClick(record); + } + + this.isSelected(record) ? this.selected(null) : this.selected(record); + this.toggleAddSelectedButton(); + + return true; + }, + + /** + * Deselect the record + */ + deselectImage: function () { + this.selected(null); + this.toggleAddSelectedButton(); + }, + + /** + * Get the selected record + * @returns {Object} + */ + getSelected: function () { + return this.selected(); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Toggle add selected button + */ + toggleAddSelectedButton: function () { + if (this.selected() === null) { + this.hideAddSelectedAndDeleteButon(); + } else { + $(this.addSelectedBtnSelector).removeClass('no-display'); + $(this.deleteSelectedBtnSelector).removeClass('no-display'); + } + }, + + /** + * Hide add selected and Delete button + */ + hideAddSelectedAndDeleteButon: function () { + $(this.addSelectedBtnSelector).addClass('no-display'); + $(this.deleteSelectedBtnSelector).addClass('no-display'); + }, + + /** + * @param {jQuery.event} e + * @param {Object} data + */ + reloadMediaGrid: function (e, data) { + if (data.reload) { + this.reloadGrid(); + } + + if (data.message && data.code) { + this.addMessage(data.code, data.message); + } + this.hideAddSelectedAndDeleteButon(); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + }, + + /** + * Add message + * + * @param {String} code + * @param {String} message + */ + addMessage: function (code, message) { + this.messages().add(code, message); + this.messages().scheduleCleanup(); + }, + + /** + * Listener to select directory event + * + * @param {String} path + */ + selectDirectoryHandle: function (path) { + if (this.selected() && + this.selected().directory !== path && + !this.massaction().massActionMode()) { + this.deselectImage(); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js new file mode 100644 index 0000000000000..38743c8d83d3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -0,0 +1,109 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'mage/translate' +], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/columns/image/actions', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + actionsList: [ + { + name: 'image-details', + title: $t('View Details'), + handler: 'viewImageDetails' + }, + { + name: 'edit', + title: $t('Edit'), + handler: 'editImageDetails' + }, + { + name: 'delete', + title: $t('Delete'), + handler: 'deleteImageAction' + } + ], + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize image action events + */ + initEvents: function () { + $(this.imageModel().addSelectedBtnSelector).click(function () { + image.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + }.bind(this)); + $(this.imageModel().deleteSelectedBtnSelector).click(function () { + this.deleteImageAction(this.imageModel().selected()); + }.bind(this)); + + }, + + /** + * Delete image action + * + * @param {Object} record + */ + deleteImageAction: function (record) { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction([record.id], imageDetailsUrl, deleteImageUrl); + }, + + /** + * View image details + * + * @param {Object} record + */ + viewImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryImageDetails().showImageDetailsById(recordId); + }, + + /** + * Edit image details + * + * @param {Object} record + */ + editImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryEditDetails().showEditDetailsPanel(recordId); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js new file mode 100644 index 0000000000000..f72a05b6d2709 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js @@ -0,0 +1,131 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global FORM_KEY, tinyMceEditors */ +define([ + 'jquery', + 'wysiwygAdapter', + 'underscore', + 'mage/translate' +], function ($, wysiwyg, _, $t) { + 'use strict'; + + return { + + /** + * Insert provided image in wysiwyg if enabled, or widget + * + * @param {Object} record + * @param {Object} config + * @returns {Boolean} + */ + insertImage: function (record, config) { + var targetElement; + + if (record === null) { + return false; + } + targetElement = this.getTargetElement(window.MediabrowserUtility.targetElementId); + + if (!targetElement.length) { + window.MediabrowserUtility.closeDialog(); + throw $t('Target element not found for content update'); + } + + $.ajax({ + url: config.onInsertUrl, + data: { + filename: record['encoded_id'], + 'store_id': config.storeId, + 'as_is': targetElement.is('textarea') ? 1 : 0, + 'force_static_path': targetElement.data('force_static_path') ? 1 : 0, + 'form_key': FORM_KEY + }, + context: this, + showLoader: true + }).done($.proxy(function (data) { + if (targetElement.is('textarea')) { + this.insertAtCursor(targetElement.get(0), data); + targetElement.focus(); + $(targetElement).change(); + } else { + targetElement.val(data) + .data('size', record.size) + .data('mime-type', record['content_type']) + .trigger('change'); + } + }, this)); + window.MediabrowserUtility.closeDialog(); + targetElement.focus(); + }, + + /** + * Insert image to target instance. + * + * @param {Object} element + * @param {*} value + */ + insertAtCursor: function (element, value) { + var sel, startPos, endPos, scrollTop; + + if ('selection' in document) { + //For browsers like Internet Explorer + element.focus(); + sel = document.selection.createRange(); + sel.text = value; + element.focus(); + } else if (element.selectionStart || element.selectionStart == '0') { //eslint-disable-line eqeqeq + //For browsers like Firefox and Webkit based + startPos = element.selectionStart; + endPos = element.selectionEnd; + scrollTop = element.scrollTop; + element.value = element.value.substring(0, startPos) + value + + element.value.substring(startPos, endPos) + element.value.substring(endPos, element.value.length); + element.focus(); + element.selectionStart = startPos + value.length; + element.selectionEnd = startPos + value.length + element.value.substring(startPos, endPos).length; + element.scrollTop = scrollTop; + } else { + element.value += value; + element.focus(); + } + }, + + /** + * Return opener Window object if it exists, not closed and editor is active + * + * @param {String} targetElementId + * return {Object|null} + */ + getMediaBrowserOpener: function (targetElementId) { + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId) && !_.isUndefined(tinyMceEditors) && + !tinyMceEditors.get(targetElementId).getMediaBrowserOpener().closed + ) { + return tinyMceEditors.get(targetElementId).getMediaBrowserOpener(); + } + + return null; + }, + + /** + * Get target element + * + * @param {String} targetElementId + * @returns {*|n.fn.init|jQuery|HTMLElement} + */ + getTargetElement: function (targetElementId) { + var opener; + + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId)) { + opener = this.getMediaBrowserOpener(targetElementId) || window; + targetElementId = tinyMceEditors.get(targetElementId).getMediaBrowserTargetElementId(); + + return $(opener.document.getElementById(targetElementId)); + } + + return $('#' + targetElementId); + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js new file mode 100644 index 0000000000000..659fcc0cdcfda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js @@ -0,0 +1,49 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/masonry', + 'jquery' +], function (Masonry, $) { + 'use strict'; + + return Masonry.extend({ + defaults: { + modules: { + provider: '${ $.provider }' + } + }, + + /** + * Init component + * + * @return {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize events + */ + initEvents: function () { + $(window).on('folderDeleted.enhancedMediaGallery', this.reloadGrid.bind(this)); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js new file mode 100644 index 0000000000000..a49303669edc8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js @@ -0,0 +1,147 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'mage/translate', + 'text!Magento_MediaGalleryUi/template/grid/massactions/cancelButton.html' +], function ($, Component, $t, cancelMassActionButton) { + 'use strict'; + + return Component.extend({ + defaults: { + pageActionsSelector: '.page-actions-buttons', + gridSelector: '[data-id="media-gallery-masonry-grid"]', + originDeleteSelector: null, + originCancelEvent: null, + cancelMassactionButton: cancelMassActionButton, + isCancelButtonInserted: false, + deleteButtonSelector: '#delete_massaction', + addSelectedButtonSelector: '#add_selected', + cancelMassactionButtonSelector: '#cancel', + standAloneTitle: 'Manage Gallery', + slidePanelTitle: 'Media Gallery', + defaultTitle: null, + contextButtonSelector: '.three-dots', + buttonsIds: [ + '#delete_folder', + '#create_folder', + '#upload_image', + '#search_adobe_stock', + '.three-dots', + '#add_selected' + ], + massactionModeTitle: $t('Select Images to Delete') + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + + return this; + }, + + /** + * Switch massaction view state per active mode. + */ + switchView: function () { + this.changePageTitle(); + this.switchButtons(); + }, + + /** + * Hide or show buttons per active mode. + */ + switchButtons: function () { + + if (this.massActionMode()) { + this.activateMassactionButtonView(); + } else { + this.revertButtonsToDefaultView(); + } + }, + + /** + * Sets buttons to default regular -mode view. + */ + revertButtonsToDefaultView: function () { + $(this.deleteButtonSelector).replaceWith(this.originDeleteSelector); + + if (!this.isCancelButtonInserted) { + $('#cancel_massaction').replaceWith(this.originCancelEvent); + } else { + $(this.cancelMassactionButtonSelector).addClass('no-display'); + $('#cancel_massaction').remove(); + } + + $.each(this.buttonsIds, function (key, value) { + $(value).removeClass('no-display'); + }); + + $(this.addSelectedButtonSelector).addClass('no-display'); + $(this.deleteButtonSelector) + .addClass('media-gallery-actions-buttons') + .removeClass('primary'); + }, + + /** + * Activate mass action buttons view + */ + activateMassactionButtonView: function () { + this.originDeleteSelector = $(this.deleteButtonSelector).clone(); + $(this.originDeleteSelector).click(function () { + $(window).trigger('massAction.MediaGallery'); + }); + this.originCancelEvent = $('#cancel').clone(true, true); + + $.each(this.buttonsIds, function (key, value) { + $(value).addClass('no-display'); + }); + + $(this.deleteButtonSelector) + .removeClass('media-gallery-actions-buttons') + .text($t('Delete Selected')) + .addClass('primary'); + + if (!$(this.cancelMassactionButtonSelector).length) { + $(this.pageActionsSelector).append(this.cancelMassactionButton); + this.isCancelButtonInserted = true; + } else { + $(this.cancelMassactionButtonSelector).replaceWith(this.cancelMassactionButton); + } + $('#cancel_massaction').on('click', function () { + $(window).trigger('terminateMassAction.MediaGallery'); + }).applyBindings(); + + $(this.deleteButtonSelector).off('click').on('click', function () { + $(this.deleteButtonSelector).trigger('massDelete'); + }.bind(this)); + + }, + + /** + * Change page title per active mode. + */ + changePageTitle: function () { + var title = $('h1:contains(' + this.standAloneTitle + ')'), + titleSelector = title.length === 1 ? title : $('h1:contains(' + this.slidePanelTitle + ')'); + + if (this.massActionMode()) { + this.defaultTitle = titleSelector.text(); + titleSelector.text(this.massactionModeTitle); + } else { + titleSelector = $('h1:contains(' + this.massactionModeTitle + ')'); + titleSelector.text(this.defaultTitle); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js new file mode 100644 index 0000000000000..8114305a3b29c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -0,0 +1,151 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'uiLayout', + 'underscore', + 'Magento_Ui/js/modal/alert', + 'mage/translate' +], function ($, Component, DeleteImages, Layout, _, uiAlert, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + deleteImagesSelector: '#delete_massaction', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + modules: { + massactionView: '${ $.name }_view', + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/massaction/massactionView', + name: '${ $.name }_view' + } + ], + imports: { + imageItems: '${ $.mediaGalleryProvider }:data.items' + }, + listens: { + imageItems: 'checkButtonVisibility' + }, + exports: { + massActionMode: '${ $.name }_view:massActionMode' + } + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + this.initView(); + this.initEvents(); + + return this; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + Layout(this.viewConfig); + + return this; + }, + + /** + * Initilize massactions events for media gallery grid. + */ + initEvents: function () { + $(window).on('massAction.MediaGallery', function () { + if (this.massActionMode()) { + return; + } + this.imageModel().selected(null); + this.massActionMode(true); + this.switchMode(); + }.bind(this)); + + $(window).on('terminateMassAction.MediaGallery', function () { + if (!this.massActionMode()) { + return; + } + + this.massActionMode(false); + this.switchMode(); + }.bind(this)); + }, + + /** + * Return total selected items. + */ + getSelectedCount: function () { + if (this.massActionMode() && !_.isNull(this.imageModel().selected())) { + return Object.keys(this.imageModel().selected()).length; + } + + return 0; + }, + + /** + * If images records less than one, disable "delete images" button + */ + checkButtonVisibility: function () { + if (this.imageItems.length < 1) { + $(this.deleteImagesSelector).addClass('disabled'); + } else { + $(this.deleteImagesSelector).removeClass('disabled'); + } + }, + + /** + * Switch massaction per current event. + */ + switchMode: function () { + this.massactionView().switchView(); + this.handleDeleteAction(); + }, + + /** + * Change Default behavior of delete image to bulk deletion. + */ + handleDeleteAction: function () { + if (this.massActionMode()) { + $(this.massactionView().deleteButtonSelector).on('massDelete', function () { + if (this.getSelectedCount() < 1) { + uiAlert({ + content: $t('You need to select at least one image') + }); + + } else { + DeleteImages.deleteImageAction( + this.imageModel().selected(), + this.mediaGalleryImageDetails().imageDetailsUrl, + this.imageModel().deleteImageUrl + ).then(function (response) { + if (response.status === 'canceled') { + return; + } + this.imageModel().selected({}); + this.massActionMode(false); + this.switchMode(); + }.bind(this)); + } + }.bind(this)); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js new file mode 100644 index 0000000000000..7116784f41a0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/messages', + messageDelay: 5, + messages: [] + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'messages' + ]); + + return this; + }, + + /** + * Get messages + * + * @returns {Array} + */ + get: function () { + return this.messages(); + }, + + /** + * Add message + * + * @param {String} type + * @param {String} message + */ + add: function (type, message) { + this.messages.push({ + code: type, + message: message + }); + }, + + /** + * Clear messages + */ + clear: function () { + this.messages.removeAll(); + }, + + /** + * Schedule message cleanup + * + * @param {Number} delay + */ + scheduleCleanup: function (delay) { + // eslint-disable-next-line no-unused-vars + var timerId; + + delay = delay || this.messageDelay; + + timerId = setTimeout(function () { + clearTimeout(timerId); + this.clear(); + }.bind(this), Number(delay) * 1000); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js new file mode 100644 index 0000000000000..15f62d6a7efd1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/sortBy' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + columnIndexMap: {} + }, + + /** + * Prepared sort order options + */ + preparedOptions: function (columns) { + var index = 0, + sortBy; + + if (columns && columns.length > 0) { + columns.map(function (column) { + if (column.sortable === true) { + sortBy = column['sort_by'] || {}; + + if (sortBy.excluded) { + return; + } + + this.options.push({ + value: column.index, + label: column.label, + sortByField: sortBy.field, + sortDirection: sortBy.direction + }); + + this.columnIndexMap[column.index] = index++; + + this.isVisible(true); + } else { + this.isVisible(false); + } + }.bind(this)); + } + }, + + /** + * Apply changes + */ + applyChanges: function () { + var column = this.getColumn(this.selectedOption()); + + this.applied({ + field: column.sortByField || this.selectedOption(), + direction: column.sortDirection || this.sorting + }); + }, + + /** + * Get column by index + * + * @param {String} optionIndex + * @returns {Object} + */ + getColumn: function (optionIndex) { + return this.options[this.columnIndexMap[optionIndex]]; + }, + + /** + * Select default option + */ + selectDefaultOption: function () { + this.selectedOption(this.options[0].value); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js new file mode 100644 index 0000000000000..3b69ca07f5771 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js @@ -0,0 +1,245 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'jquery', + 'underscore', + 'Magento_Ui/js/lib/validation/validator', + 'mage/translate', + 'jquery/file-uploader' +], function (Component, $, _, validator, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + imageUploadInputSelector: '#image-uploader-form', + directoriesPath: 'media_gallery_listing.media_gallery_listing.media_gallery_directories', + actionsPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url', + messagesPath: 'media_gallery_listing.media_gallery_listing.messages', + imageUploadUrl: '', + acceptFileTypes: '', + allowedExtensions: '', + maxFileSize: '', + maxFileNameLength: 90, + loader: false, + modules: { + directories: '${ $.directoriesPath }', + actions: '${ $.actionsPath }', + mediaGridMessages: '${ $.messagesPath }', + sortBy: '${ $.sortByName }', + listingPaging: '${ $.listingPagingName }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super().observe( + [ + 'loader', + 'count' + ] + ); + + return this; + }, + + /** + * Initializes file upload library + */ + initializeFileUpload: function () { + $(this.imageUploadInputSelector).fileupload({ + url: this.imageUploadUrl, + acceptFileTypes: this.acceptFileTypes, + allowedExtensions: this.allowedExtensions, + maxFileSize: this.maxFileSize, + + /** + * Extending the form data + * + * @param {Object} form + * @returns {Array} + */ + formData: function (form) { + return form.serializeArray().concat( + [{ + name: 'isAjax', + value: true + }, + { + name: 'form_key', + value: window.FORM_KEY + }, + { + name: 'target_folder', + value: this.getTargetFolder() + }] + ); + }.bind(this), + + add: function (e, data) { + if (!this.isSizeExceeded(data.files[0]).passed) { + this.addValidationErrorMessage('Cannot upload "' + data.files[0].name + + '". File exceeds maximum file size limit.'); + + return; + } else if (!this.isFileNameLengthExceeded(data.files[0]).passed) { + this.addValidationErrorMessage('Cannot upload "' + data.files[0].name + + '". Filename is too long, must be 90 characters or less.'); + + return; + } + + this.showLoader(); + this.count(1); + data.submit(); + }.bind(this), + + stop: function () { + this.openNewestImages(); + this.mediaGridMessages().scheduleCleanup(); + }.bind(this), + + start: function () { + this.mediaGridMessages().clear(); + }.bind(this), + + done: function (e, data) { + var response = data.jqXHR.responseJSON; + + if (!response) { + this.showErrorMessage(data, $t('Could not upload the asset.')); + + return; + } + + if (!response.success) { + this.showErrorMessage(data, response.message); + + return; + } + this.showSuccessMessage(data); + this.hideLoader(); + this.actions().reloadGrid(); + }.bind(this) + }); + }, + + /** + * Add error message after validation error. + * + * @param {String} message + */ + addValidationErrorMessage: function (message) { + this.mediaGridMessages().add( + 'error', + $t(message) + ); + + this.count() < 2 || this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Checks if size of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isSizeExceeded: function (file) { + return validator('validate-max-size', file.size, this.maxFileSize); + }, + + /** + * Checks if name length of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isFileNameLengthExceeded: function (file) { + return validator('max_text_length', file.name, this.maxFileNameLength); + }, + + /** + * Go to recently uploaded images if at least one uploaded successfully + */ + openNewestImages: function () { + this.mediaGridMessages().get().each(function (message) { + if (message.code === 'success') { + this.actions().deselectImage(); + this.sortBy().selectDefaultOption(); + this.listingPaging().goFirst(); + + return false; + } + }.bind(this)); + }, + + /** + * Show error meassages with file name. + * + * @param {Object} data + * @param {String} message + */ + showErrorMessage: function (data, message) { + data.files.each(function (file) { + this.mediaGridMessages().add( + 'error', + file.name + ': ' + $t(message) + ); + }.bind(this)); + + this.hideLoader(); + }, + + /** + * Show success message, and files counts + */ + showSuccessMessage: function () { + var prefix = this.count() === 1 ? 'an image' : this.count() + ' images'; + + this.mediaGridMessages().messages.remove(function (item) { + return item.code === 'success'; + }); + this.mediaGridMessages().add('success', $t('Successfully uploaded ' + prefix)); + this.count(this.count() + 1); + + }, + + /** + * Gets Media Gallery selected folder + * + * @returns {String} + */ + getTargetFolder: function () { + + if (_.isUndefined(this.directories().activeNode()) || + _.isNull(this.directories().activeNode())) { + return '/'; + } + + return this.directories().activeNode(); + }, + + /** + * Shows spinner loader + */ + showLoader: function () { + this.loader(true); + }, + + /** + * Hides spinner loader + */ + hideLoader: function () { + this.loader(false); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js new file mode 100644 index 0000000000000..c7ca95bed863c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiElement', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'Magento_MediaGalleryUi/js/action/saveDetails', + 'mage/validation' +], function ($, _, Element, deleteImageWithDetailConfirmation, addSelected, saveDetails) { + 'use strict'; + + return Element.extend({ + defaults: { + modalSelector: '', + modalWindowSelector: '', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + template: 'Magento_MediaGalleryUi/image/actions', + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + $(window).on('fileDeleted.enhancedMediaGallery', this.closeViewDetailsModal.bind(this)); + + return this; + }, + + /** + * Close the images details modal + */ + closeModal: function () { + var modalElement = $(this.modalSelector), + modalWindow = $(this.modalWindowSelector); + + if (!modalWindow.hasClass('_show') || !modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Opens the image edit panel + */ + editImageAction: function () { + var record = this.imageModel().getSelected().id; + + this.mediaGalleryEditDetails().showEditDetailsPanel(record); + }, + + /** + * Delete image action + */ + deleteImageAction: function () { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction( + [this.imageModel().getSelected().id], + imageDetailsUrl, + deleteImageUrl + ); + }, + + /** + * Save image details action + */ + saveImageDetailsAction: function () { + var saveDetailsUrl = this.mediaGalleryEditDetails().saveDetailsUrl, + modalElement = $(this.modalSelector), + form = modalElement.find('#image-edit-details-form'), + imageId = this.imageModel().getSelected().id, + keywords = this.mediaGalleryEditDetails().selectedKeywords(), + imageDetails = this.mediaGalleryImageDetails(); + + if (form.validation('isValid')) { + saveDetails( + saveDetailsUrl, + [form.serialize(), $.param({ + 'keywords': keywords + })].join('&') + ).then(function () { + this.closeModal(); + this.imageModel().reloadGrid(); + imageDetails.removeCached(imageId); + + if (imageDetails.isActive()) { + imageDetails.showImageDetailsById(imageId); + } + }.bind(this)); + } + }, + + /** + * Add Image + */ + addImage: function () { + addSelected.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + this.closeModal(); + }, + + /** + * Close view details modal after confirm deleting image + */ + closeViewDetailsModal: function () { + this.closeModal(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js new file mode 100644 index 0000000000000..d0d37d49329e0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js @@ -0,0 +1,174 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/getDetails' +], function ($, _, Component, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-details', + modalSelector: '', + modalWindowSelector: '', + imageDetailsUrl: '/media_gallery/image/details', + images: [], + tagListLimit: 7, + showAllTags: false, + image: null, + modules: { + mediaGridMessages: '${ $.mediaGridMessages }' + } + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'showAllTags' + ]); + + return this; + }, + + /** + * Show image details by ID + * + * @param {String} imageId + */ + showImageDetailsById: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }, + + /** + * Open image details popup + */ + openImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.showAllTags(false); + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Get tag text + * + * @param {String} tagText + * @param {Number} tagIndex + * @return {String} + */ + getTagText: function (tagText, tagIndex) { + return tagText + (this.image().tags.length - 1 === tagIndex ? '' : ','); + }, + + /** + * Show all image tags + */ + showMoreImageTags: function () { + this.showAllTags(true); + }, + + /** + * Is value an object + * + * @param {*} value + * @returns {Boolean} + */ + isArray: function (value) { + return _.isArray(value); + }, + + /** + * Get name and number text for used in link + * + * @param {Object} item + * @returns {String} + */ + getUsedInText: function (item) { + return item.name + '(' + item.number + ')'; + }, + + /** + * Get filter url + * + * @param {String} link + */ + getFilterUrl: function (link) { + return link + '?filters[asset_id]=' + this.image().id; + }, + + /** + * Check if details modal is active + * @return {Boolean} + */ + isActive: function () { + return $(this.modalWindowSelector).hasClass('_show'); + }, + + /** + * Remove image details + * + * @param {String} id + */ + removeCached: function (id) { + delete this.images[id]; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js new file mode 100644 index 0000000000000..c31bc848bdc70 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js @@ -0,0 +1,228 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'uiLayout', + 'Magento_Ui/js/lib/key-codes', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'mage/validation' +], function ($, _, Component, layout, keyCodes, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-edit', + modalSelector: '.media-gallery-edit-image-details-modal', + imageEditDetailsUrl: '/media_gallery/image/details', + saveDetailsUrl: '/media_gallery/image/saveDetails', + images: [], + image: null, + keywordOptions: [], + selectedKeywords: [], + newKeyword: '', + newKeywordSelector: '#keyword', + modules: { + mediaGridMessages: '${ $.mediaGridMessages }', + keywordsSelect: '${ $.name }_keywords' + }, + viewConfig: [ + { + component: 'Magento_Ui/js/form/element/ui-select', + name: '${ $.name }_keywords', + template: 'ui/grid/filters/elements/ui-select', + disableLabel: true + } + ], + exports: { + keywordOptions: '${ $.name }_keywords:options' + }, + links: { + selectedKeywords: '${ $.name }_keywords:value' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super().initView(); + + return this; + }, + + /** + * Add a new keyword to select + */ + addKeyword: function () { + var options = this.keywordOptions(), + selected = this.selectedKeywords(), + newKeywordField = $(this.newKeywordSelector); + + newKeywordField.validation(); + + if (!newKeywordField.validation('isValid') || this.newKeyword() === '') { + return; + } + + options.push(this.getOptionForKeyword(this.newKeyword())); + selected.push(this.newKeyword()); + this.newKeyword(''); + + this.keywordOptions(options); + this.selectedKeywords(selected); + }, + + /** + * Create an option object based on keyword string + * + * @param {String} keyword + * @returns {Object} + */ + getOptionForKeyword: function (keyword) { + return { + 'is_active': 1, + level: 1, + value: keyword, + label: keyword + }; + }, + + /** + * Convert array of keywords to options format + * + * @param {Array} tags + */ + setKeywordOptions: function (tags) { + var options = []; + + tags.forEach(function (tag) { + options.push(this.getOptionForKeyword(tag)); + }.bind(this)); + + this.keywordOptions(options); + this.selectedKeywords(tags); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'keywordOptions', + 'selectedKeywords', + 'newKeyword' + ]); + + return this; + }, + + /** + * Get image details by ID + * + * @param {String} imageId + */ + showEditDetailsPanel: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageEditDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openEditImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }, + + /** + * Open edit image details popup + */ + openEditImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.setKeywordOptions(this.image().tags); + this.newKeyword(''); + + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Handle Enter key event to save image details + * + * @param {Object} data + * @param {jQuery.Event} event + * @returns {Boolean} + */ + handleEnterKey: function (data, event) { + var modalElement = $(this.modalSelector), + key = keyCodes[event.keyCode]; + + if (key === 'enterKey') { + event.preventDefault(); + modalElement.find('.page-action-buttons button.save').click(); + } + + return true; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js new file mode 100644 index 0000000000000..127f1676015f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-description', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\n\ ]+$|^$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), ' + + 'dots (.), commas(,), underscores (_), dashes (-), and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js new file mode 100644 index 0000000000000..47fa5b19781bc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($, validate, $t) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-keyword', function (value) { + return /^[a-zA-Z0-9\-\_\.\,]+$|^$/i.test(value); + + }, $t('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_) and dashes(-) on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js new file mode 100644 index 0000000000000..1429be64b7d12 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-title', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\ ]+$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_), dashes(-) and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html new file mode 100644 index 0000000000000..4a8350231a0fd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html @@ -0,0 +1,45 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-wrap" collapsible> + <div class="mediagallery-massaction-checkbox" if="isMassActionMode()"> + <input type="checkbox" attr="{ 'data-ui-id': $row().title }" visible="isMassActionMode()" ko-checked="isSelected($row())" click="function () { return select($row()); }"/> + </div> + <div class="media-gallery-image"> + <div data-row="file" + class="masonry-image-block media-gallery-image-block" + attr="'data-id': $col.getId($row())" + css="{ selected: isSelected($row()) }" + click="function(){ clickOnImage($row(), $collapsible.opened()) }" + > + <img attr="src: $col.getUrl($row()), alt: $col.getImageAlt($row())" + class="media-gallery-image-column" + data-role="thumbnail"/> + </div> + <ul class="action-menu" css="_active: $collapsible.opened"> + <scope args="actions"> + <render args="template"/> + </scope> + </ul> + </div> + <div class="masonry-image-description"> + <ul class="media-gallery-image-details"> + <li class="name" data-ui-id="title" text="$row().title"></li> + <li class="source"> + <img if="$row().source" class="adobe-stock-icon" attr="{ src: $row().source }"/> + </li> + <li class="type" data-ui-id="content-type" text="$row().content_type"></li> • + <li class="dimensions" data-ui-id="dimensions" text="$row().width + 'x' + $row().height"></li> + </ul> + <div class="media-gallery-image-actions"> + <div class="action-select-wrap"> + <span class="three-dots" ifnot="isMassActionMode()" + toggleCollapsible + click="function () { clickOnThreeDots($row(), $collapsible.opened()); }"></span> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html new file mode 100644 index 0000000000000..042e119b9f40e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<each args="{ data: actionsList, as: 'action' }"> + <li> + <a class="action-menu-item" href="" text="action.title" + click="$parent[action.handler].bind($parent, $row())" + attr="{'data-action': 'item-' + action.name}"> + </a> + </li> +</each> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html new file mode 100644 index 0000000000000..da835952e2f23 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html @@ -0,0 +1,10 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="media-directory-container"> + <div id="media-gallery-directory-tree"></div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html new file mode 100644 index 0000000000000..d1840fdb3dc8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html @@ -0,0 +1,24 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__field admin__media-gallery-image-checkbox" visible="visible" css="$data.additionalClasses"> + <div class="admin__field-control"> + <label class="admin__form-field-label" if="$data.label" attr="for: uid"> + <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> + </label> + </div> + <div class="admin__field admin__field-option"> + <input type="checkbox" + class="admin__control-checkbox" + ko-checked="$data.checked" + disable="disabled" + ko-value="value" + hasFocus="focused" + attr="id: uid, name: inputName"/> + + <label class="admin__field-label" text="description" attr="for: uid"/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html new file mode 100644 index 0000000000000..cce859f331d9a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html @@ -0,0 +1,133 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<ifnot args="disableLabel"> + <label class="admin__form-field-label" attr="{for: uid}"> + <span translate="label"></span> + </label> +</ifnot> +<div class="admin__action-multiselect-wrap action-select-wrap media-gallery-asset-ui-select-filter" + tabindex="0" attr="{id: uid}" css="{_active: listVisible,'admin__action-multiselect-tree': isTree()}" + event="{focusin: onFocusIn,focusout: onFocusOut,keydown: keydownSwitcher}" outerClick="outerClick.bind($data)"> + <ifnot args="chipsEnabled"> + <div class="action-select admin__action-multiselect" + data-role="advanced-select" + css="{_active: listVisible}" + click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" data-role="selected-option" + ifnot="validationLoading" css="{warning: warn().length}" text="setCaption()"> + </div> + <button if="isRemoveSelectedIcon && hasData() || !validationLoading" class="action-close" + type="button" data-action="remove-selected-item" tabindex="-1" click="clear"> + <span class="action-close-text" translate="'Close'"></span> + </button> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="validationLoading" + if="validationLoading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + </div> + </ifnot> + <if args="chipsEnabled"> + <div class="action-select admin__action-multiselect" data-role="advanced-select" + css="{_active: listVisible}" click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" visible="!hasData()" + translate="selectedPlaceholders.defaultPlaceholder"> + </div> + <each args="{ data: getSelected(), as: 'option'}"> + <span class="admin__action-multiselect-crumb"> + <span text="label"> + </span> + <button class="action-close" type="button" data-action="remove-selected-item" + tabindex="-1" click="$parent.removeSelected.bind($parent, value)"> + <span class="action-close-text" translate="'Close'"></span> + </button> + </span> + </each> + </div> + </if> + <div class="action-menu" css="{ _active: listVisible}"> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loading" if="loading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + <if args="filterOptions"> + <div class="admin__action-multiselect-search-wrap"> + <input class="admin__control-text admin__action-multiselect-search" data-role="advanced-select-text" + type="text" event="{keydown: filterOptionsKeydown}" attr="{id: uid+2, placeholder: filterPlaceholder}" + ko-focused="filterOptionsFocus" ko-value="filterInputValue" data-bind="valueUpdate:'afterkeydown'"> + <label class="admin__action-multiselect-search-label" + data-action="advanced-select-search" + attr="{for: uid+2}"> + </label> + <div if="itemsQuantity" + text="itemsQuantity" + class="admin__action-multiselect-search-count"> + </div> + </div> + <div ifnot="options().length" + class="admin__action-multiselect-empty-area"> + <ul text="emptyOptionsHtml"/> + </div> + </if> + <ul class="admin__action-multiselect-menu-inner _root" + event="{mousemove: function(data, event){onMousemove($data, $index(), event)}, + scroll: function(data, event){onScrollDown(data, event)}}"> + <each args="{ data: options, as: 'option'}"> + <li class="admin__action-multiselect-menu-inner-item _root" + css="{ _parent: $data.optgroup }" + data-role="option-group"> + <div class="action-menu-item" + css="{ + _selected: $parent.isSelectedValue(option), + _hover: $parent.isHovered(option, $element), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), + _unclickable: $parent.isLabelDecoration($data), + _last: $parent.addLastElement($data), + '_with-checkbox': $parent.showCheckbox + }" + click="function(data, event){ + $parent.toggleOptionSelected($data, $index(), event); + }" + data-bind="clickBubble:false"> + <if args="$data.optgroup && $parent.showOpenLevelsActionIcon"> + <div class="admin__action-multiselect-dropdown" + click="function(event){ $parent.showLevels($data); $parent.openChildLevel($data, $element, event);}" + data-bind="clickBubble:false"> + </div> + </if> + <if args="$parent.showCheckbox"> + <input class="admin__control-checkbox" type="checkbox" + tabindex="-1" attr="{ 'checked': $parent.isSelected(option.value) }"> + </if> + <label class="admin__action-multiselect-label"> + <span text="option.label"></span> + <img if="$parent.getPath(option)" + class="admin__action-multiselect-item-path" + attr="{ src: option.path }"/> + </label> + </div> + <if args="$data.optgroup"> + <render args="{name: $parent.optgroupTmpl, data: {root: $parent, current: $data}}" ></render> + </if> + </li> + </each> + </ul> + <if args="$data.closeBtn"> + <div class="admin__action-multiselect-actions-wrap"> + <button class="action-default" + data-action="close-advanced-select" + type="button" + click="outerClick"> + <span translate="closeBtnLabel"></span> + </button> + </div> + </if> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html new file mode 100644 index 0000000000000..243ed1c2a5dc4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html @@ -0,0 +1,10 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<button id="cancel_massaction" type="button" class="cancel"> + <span data-bind="i18n: 'Cancel'"/> +</button> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html new file mode 100644 index 0000000000000..5bbdafebe4095 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div visible="massActionMode()" class="mediagallery-massaction-items-count"> + <div class="selected_count_text">(<b><text args="getSelectedCount()"/> Selected</b>) </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html new file mode 100644 index 0000000000000..1ec084e223e98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<ul class="messages"> + <div class="messages" outereach="messages"> + <div attr="class: 'message message-'+code"> + <div data-ui-id="messages-message-error"> + <span text="message"></span> + </div> + </div> + </div> +</ul> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html new file mode 100644 index 0000000000000..fb7334a7b0d06 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html @@ -0,0 +1,32 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__data-grid-header" data-role="masonry-main-toolbar" afterRender="$data.setToolbarNode"> + <div class="admin__data-grid-header-row"> + <div class="admin__data-grid-actions-wrap" each="getRegion('dataGridActions')" render=""/> + <each args="getRegion('dataGridFilters')" render=""/> + </div> + <div class="admin__data-grid-header-row row row-gutter"> + <div class="col-xs-2" if="hasChild('listing_massaction')" ko-scope="requestChild('listing_massaction')" render=""/> + <div css=" + 'col-xs-10': hasChild('listing_massaction'), + 'col-xs-12': !hasChild('listing_massaction')"> + <div class="row"> + <div class="col-xs-4"> + <div class="masonry-results-number" ko-scope="requestChild('listing_paging')"> + <render args="totalTmpl"/> + </div> + <each args="getRegion('sorting')" render=""/> + </div> + <div class="col-xs-8" ko-scope="requestChild('listing_paging')"> + <div render=""/> + </div> + </div> + </div> + </div> +</div> + +<render args="stickyTmpl" if="$data.sticky"/> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html new file mode 100644 index 0000000000000..6d5580b1aad6e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html @@ -0,0 +1,17 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-image-uploader-container"> + <form id="image-uploader-form" class="no-display" method="POST" enctype="multipart/form-data"> + <input afterRender="initializeFileUpload" id="image-uploader-input" type="file" name="image" + multiple="multiple"/> + </form> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loader"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html new file mode 100644 index 0000000000000..8ecaf0bd2a019 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html @@ -0,0 +1,12 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<each args="{ data: actionsList, as: 'action' }"> + <button type="button" click="$parent[action.handler].bind($parent)" + attr="{class: action.classes, id: 'image-details-action-' + action.name, title: $t(action.title)}"> + <span translate="action.title"></span> + </button> +</each> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html new file mode 100644 index 0000000000000..a397bad0af698 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html @@ -0,0 +1,65 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="image-details" if="image"> + <div class="image-details-image"> + <img attr="src: image().image_url"/> + </div> + <div class="image-details-sidebar"> + <div class="image-details-section"> + <h3 class="image-title" text="image().title"></h3> + <div class="image-type"> + <span class="source"><img if="image().source" class="adobe-stock-icon" attr="{ src: image().source }" /></span> + <span class="type" data-ui-id="content-type" text="image().content_type"></span> + </div> + </div> + <div class="filename image-details-section"> + <h3 translate="'Filename'"></h3> + <p text="image().path"></p> + </div> + <div class="general-details image-details-section" if="image().details"> + <h3 translate="'Details'"></h3> + <div class="attributes"> + <each args="image().details"> + <div class="attribute" if="value"> + <span class="title" translate="title"></span> + <ifnot args="$parent.isArray(value)"> + <div class="value" text="value"></div> + </ifnot> + <if args="$parent.isArray(value)"> + <each args="{ data: value, as: 'item'}"> + <div class="value"> + <a attr="href: $parents[1].getFilterUrl(item.link)" + text="$parents[1].getUsedInText(item)"></a> + </br> + </div> + </each> + </if> + </div> + </each> + </div> + </div> + <div class="description image-details-section" if="image().description"> + <h3 translate="'Description'"></h3> + <p text="image().description"></p> + </div> + <div class="tags image-details-section" if="image().tags.length"> + <h3 translate="'Tags'"></h3> + <div class="tags-list" css="{'show-all-tags': showAllTags}"> + <each args="data: image().tags, as: '$tag'"> + <span class="tag-item" text="$parent.getTagText($tag, $index())" + css="{'show-more-item': ($index() + 1) > $parent.tagListLimit}"></span> + </each> + </div> + <div class="show-more-link-container"> + <a href="#" class="show-more-link" if="image().tags.length > tagListLimit" + translate="'Show More'" click="showMoreImageTags"></a> + </div> + </div> + + <each args="getRegion('additional_image_details')" render=""/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html new file mode 100644 index 0000000000000..e8448e1a64aef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html @@ -0,0 +1,74 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="edit-image-details" if="image"> + <fieldset class="admin__fieldset"> + <input type="hidden" ko-value="image().id" data-ui-id="id" name="id"/> + <div class="admin__field _required"> + <label for="title" class="admin__field-label"> + <span translate="'Title'"></span> + </label> + <div class="admin__field-control"> + <input type="text" id="title" data-ui-id="title" name="title" placeholder="Title" + class="admin__control-text required-entry minimum-length-1 maximum-length-128" + ko-value="image().title" event="{keypress: handleEnterKey}" + data-validate="{'required':true,'validate-image-title':true, 'validate-length':true}"/> + </div> + </div> + <div class="admin__field"> + <label for="path" class="admin__field-label"> + <span translate="'Filename'"></span> + </label> + <div class="admin__field-control path-display"> + <span data-ui-id="path" id="path" text="image().path"></span> + </div> + </div> + <div class="admin__field"> + <label for="description" class="admin__field-label"> + <span translate="'Description'"></span> + </label> + <div class="admin__field-control"> + <textarea id="description" + data-ui-id="description" + name="description" + class="admin__control-textarea minimum-length-0 maximum-length-500" + rows="7" cols="80" + ko-value="image().description" + data-validate="{'validate-image-description':true, 'validate-length':true}"></textarea> + </div> + </div> + <div class="admin__field"> + <label class="admin__field-label"> + <span translate="'Tags'"></span> + </label> + <div class="admin__field-control"> + <div class="admin__field"> + <scope args="keywordsSelect"> + <render args="template"/> + </scope> + </div> + <div class="admin__field"> + <div class="admin__field-control admin__field-option admin__control-grouped"> + <div class="admin__field admin__field-group-additional"> + <div class="admin__field-control"> + <input type="text" id="keyword" data-ui-id="keyword" name="keyword" placeholder="New Keyword" + class="admin__control-text minimum-length-0 maximum-length-128" ko-value="newKeyword" + data-validate="{'validate-image-keyword': true, 'validate-length': true}"/> + </div> + </div> + <div class="admin__field admin__field-group-additional admin__field-small"> + <div class="admin__field-control"> + <button type="button" data-ui-id="add-keyword" class="action-basic" click="addKeyword"> + <span translate="'Add New Tag'"></span> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + </fieldset> +</div> diff --git a/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php new file mode 100644 index 0000000000000..a516ac927fd2d --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUiApi\Api; + +/** + * Class responsible to provide API access to system configuration related to the Media Gallery + */ +interface ConfigInterface +{ + /** + * Check if grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool; +} diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md new file mode 100644 index 0000000000000..005a445c68b2a --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUiApi module + +The Magento_MediaGalleryUiApi module is responsible for the media gallery user interface (UI) implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUiApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUiApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json new file mode 100644 index 0000000000000..f8d5ef11058c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-ui-api", + "description": "Magento module responsible for the media gallery UI implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUiApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUiApi/etc/module.xml b/app/code/Magento/MediaGalleryUiApi/etc/module.xml new file mode 100644 index 0000000000000..cf62515ff92b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/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_MediaGalleryUiApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryUiApi/registration.php b/app/code/Magento/MediaGalleryUiApi/registration.php new file mode 100644 index 0000000000000..b3ee130a1c510 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUiApi', __DIR__); diff --git a/composer.json b/composer.json index c15dc4140f6eb..5b39c1b3f75ea 100644 --- a/composer.json +++ b/composer.json @@ -205,6 +205,20 @@ "magento/module-media-content-cms": "*", "magento/module-media-gallery": "*", "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-ui": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-integration": "*", + "magento/module-media-gallery-synchronization": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-synchronization": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-content-synchronization-catalog": "*", + "magento/module-media-content-synchronization-cms": "*", + "magento/module-media-gallery-metadata": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-catalog-ui": "*", + "magento/module-media-gallery-cms-ui": "*", + "magento/module-media-gallery-catalog-integration": "*", "magento/module-media-gallery-catalog": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", diff --git a/composer.lock b/composer.lock index 97ab2b12512c2..90131292fe956 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a8f3bda109a177996d409f39acfbfd9f", + "content-hash": "5b074864c62821207d8994a4aca444fe", "packages": [ { "name": "colinmollenhour/cache-backend-file", From 12140598cb1f54381539a5f2c2d7b13d900b575c Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 3 Aug 2020 11:29:40 +0100 Subject: [PATCH 53/65] Added synchronization modules --- .../Console/Command/Synchronize.php | 69 +++++++ .../MediaGallerySynchronization/LICENSE.txt | 48 +++++ .../LICENSE_AFL.txt | 48 +++++ .../Model/Consume.php | 37 ++++ .../Model/CreateAssetFromFile.php | 148 +++++++++++++++ .../Model/FetchBatches.php | 128 +++++++++++++ .../Model/FetchMediaStorageFileBatches.php | 127 +++++++++++++ .../Model/Filesystem/SplFileInfoFactory.php | 25 +++ .../Model/GetAssetFromPath.php | 106 +++++++++++ .../Model/GetAssetsIterator.php | 33 ++++ .../Model/GetContentHash.php | 27 +++ .../Model/ImportImageFileKeywords.php | 152 +++++++++++++++ .../Model/ImportMediaAsset.php | 53 ++++++ .../Model/Publish.php | 45 +++++ .../Model/ResolveNonExistedAssets.php | 111 +++++++++++ .../Model/Synchronize.php | 93 +++++++++ .../Model/SynchronizeFiles.php | 177 ++++++++++++++++++ .../Plugin/MediaGallerySyncTrigger.php | 53 ++++++ .../MediaGallerySynchronization/README.md | 14 ++ .../Integration/Model/GetContentHashTest.php | 110 +++++++++++ .../Test/Integration/_files/magento.jpg | Bin 0 -> 55303 bytes .../Test/Integration/_files/magento_2.jpg | Bin 0 -> 55303 bytes .../Test/Integration/_files/magento_3.png | Bin 0 -> 116576 bytes .../MediaGallerySynchronization/composer.json | 25 +++ .../etc/communication.xml | 14 ++ .../MediaGallerySynchronization/etc/di.xml | 54 ++++++ .../etc/module.xml | 10 + .../etc/queue_consumer.xml | 11 ++ .../etc/queue_publisher.xml | 12 ++ .../etc/queue_topology.xml | 14 ++ .../registration.php | 14 ++ .../Api/SynchronizeFilesInterface.php | 24 +++ .../Api/SynchronizeInterface.php | 21 +++ .../LICENSE.txt | 48 +++++ .../LICENSE_AFL.txt | 48 +++++ .../Model/FetchBatchesInterface.php | 26 +++ .../Model/GetContentHashInterface.php | 22 +++ .../Model/ImportFilesComposite.php | 38 ++++ .../Model/ImportFilesInterface.php | 25 +++ .../Model/SynchronizerPool.php | 51 +++++ .../MediaGallerySynchronizationApi/README.md | 13 ++ .../composer.json | 21 +++ .../MediaGallerySynchronizationApi/etc/di.xml | 10 + .../etc/module.xml | 10 + .../registration.php | 14 ++ 45 files changed, 2129 insertions(+) create mode 100644 app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php create mode 100644 app/code/Magento/MediaGallerySynchronization/LICENSE.txt create mode 100644 app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/Consume.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/Publish.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php create mode 100644 app/code/Magento/MediaGallerySynchronization/README.md create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png create mode 100644 app/code/Magento/MediaGallerySynchronization/composer.json create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/communication.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/di.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/module.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml create mode 100644 app/code/Magento/MediaGallerySynchronization/registration.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Model/GetContentHashInterface.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/README.md create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/composer.json create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/registration.php diff --git a/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..992f77a1df319 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Console\Command; + +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Framework\Console\Cli; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Synchronize files in media storage and media assets database records + */ +class Synchronize extends Command +{ + /** + * @var SynchronizeInterface + */ + private $synchronizeAssets; + + /** + * @var State $state + */ + private $state; + + /** + * @param SynchronizeInterface $synchronizeAssets + * @param State $state + */ + public function __construct( + SynchronizeInterface $synchronizeAssets, + State $state + ) { + $this->synchronizeAssets = $synchronizeAssets; + $this->state = $state; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-gallery:sync'); + $this->setDescription( + 'Synchronize media storage and media assets in the database' + ); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing assets information from media storage to database...'); + $this->state->emulateAreaCode(Area::AREA_ADMINHTML, function () { + $this->synchronizeAssets->execute(); + }); + $output->writeln('Completed assets synchronization.'); + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/LICENSE.txt b/app/code/Magento/MediaGallerySynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php new file mode 100644 index 0000000000000..79c0c9a1a803b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; + +/** + * Media gallery image synchronization queue consumer. + */ +class Consume +{ + /** + * @var SynchronizeInterface + */ + private $synchronize; + + /** + * @param SynchronizeInterface $synchronize + */ + public function __construct(SynchronizeInterface $synchronize) + { + $this->synchronize = $synchronize; + } + + /** + * Run media files synchronization. + */ + public function execute() : void + { + $this->synchronize->execute(); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php new file mode 100644 index 0000000000000..3305c5e54b861 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +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\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; +use Magento\MediaGallerySynchronizationApi\Model\GetContentHashInterface; + +/** + * Create media asset object based on the file information + */ +class CreateAssetFromFile +{ + /** + * Date format + */ + private const DATE_FORMAT = 'Y-m-d H:i:s'; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var TimezoneInterface; + */ + private $date; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetContentHashInterface + */ + private $getContentHash; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @var SplFileInfoFactory + */ + private $splFileInfoFactory; + + /** + * @param Filesystem $filesystem + * @param File $driver + * @param TimezoneInterface $date + * @param AssetInterfaceFactory $assetFactory + * @param GetContentHashInterface $getContentHash + * @param ExtractMetadataInterface $extractMetadata + * @param SplFileInfoFactory $splFileInfoFactory + */ + public function __construct( + Filesystem $filesystem, + File $driver, + TimezoneInterface $date, + AssetInterfaceFactory $assetFactory, + GetContentHashInterface $getContentHash, + ExtractMetadataInterface $extractMetadata, + SplFileInfoFactory $splFileInfoFactory + ) { + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->date = $date; + $this->assetFactory = $assetFactory; + $this->getContentHash = $getContentHash; + $this->extractMetadata = $extractMetadata; + $this->splFileInfoFactory = $splFileInfoFactory; + } + + /** + * Create and format media asset object + * + * @param string $path + * @return AssetInterface + * @throws FileSystemException + */ + public function execute(string $path): AssetInterface + { + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + $file = $this->splFileInfoFactory->create($absolutePath); + [$width, $height] = getimagesize($absolutePath); + + $metadata = $this->extractMetadata->execute($absolutePath); + + return $this->assetFactory->create( + [ + 'id' => null, + 'path' => $path, + 'title' => $metadata->getTitle() ?: $file->getBasename('.' . $file->getExtension()), + 'description' => $metadata->getDescription(), + 'createdAt' => $this->date->date($file->getCTime())->format(self::DATE_FORMAT), + 'updatedAt' => $this->date->date($file->getMTime())->format(self::DATE_FORMAT), + 'width' => $width, + 'height' => $height, + 'hash' => $this->getHash($path), + 'size' => $file->getSize(), + 'contentType' => 'image/' . $file->getExtension(), + 'source' => 'Local' + ] + ); + } + + /** + * Get hash image content. + * + * @param string $path + * @return string + * @throws FileSystemException + */ + private function getHash(string $path): string + { + return $this->getContentHash->execute($this->getMediaDirectory()->readFile($path)); + } + + /** + * Retrieve media directory instance with read access + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php b/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php new file mode 100644 index 0000000000000..efc79d3c32423 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; +use Psr\Log\LoggerInterface; + +/** + * Select data from database by provided batch size + */ +class FetchBatches implements FetchBatchesInterface +{ + private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var int + */ + private $pageSize; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FlagManager + */ + private $flagManager; + + /** + * @param FlagManager $flagManager + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + * @param int $pageSize + */ + public function __construct( + FlagManager $flagManager, + ResourceConnection $resourceConnection, + LoggerInterface $logger, + int $pageSize + ) { + $this->flagManager = $flagManager; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + $this->pageSize = $pageSize; + } + + /** + * Get data from table by batches, based on limit offset value. + * + * @param string $tableName + * @param array $columns + * @param string|null $dateColumnName + */ + public function execute(string $tableName, array $columns, ?string $dateColumnName = null): \Traversable + { + try { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName($tableName); + $totalPages = $this->getTotalPages($tableName); + + for ($page = 0; $page < $totalPages; $page++) { + $offset = $page * $this->pageSize; + $select = $connection->select() + ->from($this->resourceConnection->getTableName($tableName), $columns) + ->limit($this->pageSize, $offset); + if (!empty($dateColumnName)) { + $select = $this->addLastExecutionCondition($select, $dateColumnName); + } + yield $connection->fetchAll($select); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException( + __( + 'Could not fetch data from %tableName', + [ + 'tableName' => $tableName + ] + ) + ); + } + } + + /** + * Get where condition if last execution time set + * + * @param Select $select + * @param string $dateColumnName + * @return Select + */ + private function addLastExecutionCondition(Select $select, string $dateColumnName): Select + { + $lastExecutionTime = $this->flagManager->getFlagData(self::LAST_EXECUTION_TIME_CODE); + if (!empty($lastExecutionTime)) { + return $select->where($dateColumnName . ' > ?', $lastExecutionTime); + } + return $select; + } + + /** + * Return number of total pages by page size + * + * @param string $tableName + * @return float + */ + private function getTotalPages(string $tableName): float + { + $connection = $this->resourceConnection->getConnection(); + $total = $connection->fetchOne($connection->select()->from($tableName, 'COUNT(*)')); + return ceil($total / $this->pageSize); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php b/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php new file mode 100644 index 0000000000000..0643673ae30ab --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Psr\Log\LoggerInterface; + +/** + * Fetch files from media storage in batches + */ +class FetchMediaStorageFileBatches +{ + /** + * @var GetAssetsIterator + */ + private $getAssetsIterator; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var File + */ + private $driver; + + /** + * @var string + */ + private $fileExtensions; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var int + */ + private $batchSize; + + /** + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param Filesystem $filesystem + * @param GetAssetsIterator $assetsIterator + * @param File $driver + * @param int $batchSize + * @param array $fileExtensions + */ + public function __construct( + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + Filesystem $filesystem, + GetAssetsIterator $assetsIterator, + File $driver, + int $batchSize, + array $fileExtensions + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->getAssetsIterator = $assetsIterator; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->batchSize = $batchSize; + $this->fileExtensions = $fileExtensions; + } + + /** + * Return files from files system by provided size of batch + */ + public function execute(): \Traversable + { + $i = 0; + $batch = []; + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + + /** @var \SplFileInfo $file */ + foreach ($this->getAssetsIterator->execute($mediaDirectory->getAbsolutePath()) as $file) { + $relativePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath($file->getPathName()); + if (!$this->isApplicable($relativePath)) { + continue; + } + + $batch[] = $relativePath; + if (++$i == $this->batchSize) { + yield $batch; + $i = 0; + $batch = []; + } + } + if (count($batch) > 0) { + yield $batch; + } + } + + /** + * Can synchronization be applied to asset with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php new file mode 100644 index 0000000000000..1fbfae640a732 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +/** + * Creates a new file based on the file name parameter. + */ +class SplFileInfoFactory +{ + /** + * Creates SplFileInfo from filename + * + * @param string $fileName + * @return \SplFileInfo + */ + public function create(string $fileName) : \SplFileInfo + { + return new \SplFileInfo($fileName); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php new file mode 100644 index 0000000000000..5e825d57c5ce7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; + +/** + * Create media asset object based on the file information + */ +class GetAssetFromPath +{ + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var CreateAssetFromFile + */ + private $createAssetFromFile; + + /** + * @var SplFileInfoFactory + */ + private $splFileInfoFactory; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByPathsInterface $getMediaGalleryAssetByPath + * @param CreateAssetFromFile $createAssetFromFile + * @param SplFileInfoFactory $splFileInfoFactory + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByPathsInterface $getMediaGalleryAssetByPath, + CreateAssetFromFile $createAssetFromFile, + SplFileInfoFactory $splFileInfoFactory + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByPaths = $getMediaGalleryAssetByPath; + $this->createAssetFromFile = $createAssetFromFile; + $this->splFileInfoFactory= $splFileInfoFactory; + } + + /** + * Create media asset object based on the file information + * + * @param string $path + * @return AssetInterface + * @throws LocalizedException + * @throws ValidatorException + */ + public function execute(string $path): AssetInterface + { + $asset = $this->getAsset($path); + $assetFromFile = $this->createAssetFromFile->execute($path); + + if (!$asset) { + return $assetFromFile; + } + + return $this->assetFactory->create( + [ + 'id' => $asset->getId(), + 'path' => $path, + 'title' => $asset->getTitle(), + 'description' => $asset->getDescription() ?? $assetFromFile->getDescription(), + 'width' => $assetFromFile->getWidth(), + 'height' => $assetFromFile->getHeight(), + 'hash' => $assetFromFile->getHash(), + 'size' => $assetFromFile->getSize(), + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() + ] + ); + } + + /** + * Returns asset if asset already exist by provided path + * + * @param string $path + * @return AssetInterface|null + * @throws ValidatorException + * @throws LocalizedException + */ + private function getAsset(string $path): ?AssetInterface + { + $asset = $this->getAssetsByPaths->execute([$path]); + return !empty($asset) ? $asset[0] : null; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php new file mode 100644 index 0000000000000..6c0592c49f09c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +/** + * Retrieve media storage assets iterator + */ +class GetAssetsIterator +{ + /** + * Get media storage assets iterator for provided path + * + * @param string $path + * @return \RecursiveIteratorIterator + */ + public function execute(string $path): \RecursiveIteratorIterator + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $path, + \FilesystemIterator::SKIP_DOTS | + \FilesystemIterator::UNIX_PATHS | + \RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php b/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php new file mode 100644 index 0000000000000..49ff39ca4e1d7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\MediaGallerySynchronizationApi\Model\GetContentHashInterface; + +/** + * Get hashed value of image content. + */ +class GetContentHash implements GetContentHashInterface +{ + /** + * Return the hash value of the given filepath. + * + * @param string $content + * @return string + */ + public function execute(string $content): string + { + return sha1($content); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php b/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php new file mode 100644 index 0000000000000..361137ad27686 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; + +/** + * import image keywords from file metadata + */ +class ImportImageFileKeywords implements ImportFilesInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @param File $driver + * @param Filesystem $filesystem + * @param KeywordInterfaceFactory $keywordFactory + * @param ExtractMetadataInterface $extractMetadata + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param GetAssetsByPathsInterface $getAssetsByPaths + */ + public function __construct( + File $driver, + Filesystem $filesystem, + KeywordInterfaceFactory $keywordFactory, + ExtractMetadataInterface $extractMetadata, + SaveAssetsKeywordsInterface $saveAssetKeywords, + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + GetAssetsByPathsInterface $getAssetsByPaths + ) { + $this->driver = $driver; + $this->filesystem = $filesystem; + $this->keywordFactory = $keywordFactory; + $this->extractMetadata = $extractMetadata; + $this->saveAssetKeywords = $saveAssetKeywords; + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->getAssetsByPaths = $getAssetsByPaths; + } + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $keywords = []; + + foreach ($paths as $path) { + $metadataKeywords = $this->getMetadataKeywords($path); + if ($metadataKeywords !== null) { + $keywords[$path] = $metadataKeywords; + } + } + + $assets = $this->getAssetsByPaths->execute(array_keys($keywords)); + + $assetKeywords = []; + + foreach ($assets as $asset) { + $assetKeywords[] = $this->assetKeywordsFactory->create([ + 'assetId' => $asset->getId(), + 'keywords' => $keywords[$asset->getPath()] + ]); + } + + $this->saveAssetKeywords->execute($assetKeywords); + } + + /** + * Get keywords from file metadata + * + * @param string $path + * @return KeywordInterface[]|null + */ + private function getMetadataKeywords(string $path): ?array + { + $metadataKeywords = $this->extractMetadata->execute($this->getMediaDirectory()->getAbsolutePath($path)) + ->getKeywords(); + if ($metadataKeywords === null) { + return null; + } + + $keywords = []; + + foreach ($metadataKeywords as $keyword) { + $keywords[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + + return $keywords; + } + + /** + * Retrieve media directory instance with read access + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php b/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php new file mode 100644 index 0000000000000..3cac99f816d12 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; + +/** + * Import image file to the media gallery asset table + */ +class ImportMediaAsset implements ImportFilesInterface +{ + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var GetAssetFromPath + */ + private $getAssetFromPath; + + /** + * @param SaveAssetsInterface $saveAssets + * @param GetAssetFromPath $getAssetFromPath + */ + public function __construct( + SaveAssetsInterface $saveAssets, + GetAssetFromPath $getAssetFromPath + ) { + $this->saveAssets = $saveAssets; + $this->getAssetFromPath = $getAssetFromPath; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $assets = []; + + foreach ($paths as $path) { + $assets[] = $this->getAssetFromPath->execute($path); + } + + $this->saveAssets->execute($assets); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php new file mode 100644 index 0000000000000..386798d68d9df --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media gallery synchronization queue. + */ +class Publish +{ + /** + * Media gallery synchronization queue topic name. + */ + private const TOPIC_MEDIA_GALLERY_SYNCHRONIZATION = 'media.gallery.synchronization'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media content synchronization message to the message queue. + */ + public function execute() : void + { + $this->publisher->publish( + self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION, + [self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php b/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php new file mode 100644 index 0000000000000..d70547a7528e0 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; +use Psr\Log\LoggerInterface; + +/** + * Delete assets which not exist physically + */ +class ResolveNonExistedAssets +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const MEDIA_GALLERY_ASSET_PATH = 'path'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Read + */ + private $mediaDirectory; + + /** + * @var FetchBatchesInterface + */ + private $selectBatches; + + /** + * @param Filesystem $filesystem + * @param ResourceConnection $resourceConnection + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param LoggerInterface $logger + * @param FetchBatchesInterface $selectBatches + */ + public function __construct( + Filesystem $filesystem, + ResourceConnection $resourceConnection, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + LoggerInterface $logger, + FetchBatchesInterface $selectBatches + ) { + $this->filesystem = $filesystem; + $this->resourceConnection = $resourceConnection; + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->logger = $logger; + $this->selectBatches = $selectBatches; + } + + /** + * Delete assets which not existed + * + * @return void + */ + public function execute(): void + { + $columns = [self::MEDIA_GALLERY_ASSET_PATH]; + try { + foreach ($this->selectBatches->execute(self::TABLE_MEDIA_GALLERY_ASSET, $columns, null) as $batch) { + foreach ($batch as $item) { + if (!$this->getMediaDirectory()->isExist($item[self::MEDIA_GALLERY_ASSET_PATH])) { + $this->deleteAssetsByPaths->execute([$item[self::MEDIA_GALLERY_ASSET_PATH]]); + } + } + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + } + } + + /** + * Retrieve media directory instance with read permissions + * + * @return Read + */ + private function getMediaDirectory(): Read + { + if (!$this->mediaDirectory) { + $this->mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } + return $this->mediaDirectory; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php b/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..4396ea6a77736 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\SynchronizerPool; +use Psr\Log\LoggerInterface; + +/** + * Synchronize media storage and media assets database records + */ +class Synchronize implements SynchronizeInterface +{ + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizerPool + */ + private $synchronizerPool; + + /** + * @var FetchMediaStorageFileBatches + */ + private $batchGenerator; + + /** + * @var ResolveNonExistedAssets + */ + private $resolveNonExistedAssets; + + /** + * @param ResolveNonExistedAssets $resolveNonExistedAssets + * @param LoggerInterface $log + * @param SynchronizerPool $synchronizerPool + * @param FetchMediaStorageFileBatches $batchGenerator + */ + public function __construct( + ResolveNonExistedAssets $resolveNonExistedAssets, + LoggerInterface $log, + SynchronizerPool $synchronizerPool, + FetchMediaStorageFileBatches $batchGenerator + ) { + $this->resolveNonExistedAssets = $resolveNonExistedAssets; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + $this->batchGenerator = $batchGenerator; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + if (!$synchronizer instanceof SynchronizeFilesInterface) { + $failed[] = $name; + continue; + } + foreach ($this->batchGenerator->execute() as $batch) { + try { + $synchronizer->execute($batch); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + } + + $this->resolveNonExistedAssets->execute(); + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php new file mode 100644 index 0000000000000..81e9629f703f3 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; +use Psr\Log\LoggerInterface; + +/** + * Synchronize files in media storage and media assets database records + */ +class SynchronizeFiles implements SynchronizeFilesInterface +{ + /** + * Date format + */ + private const DATE_FORMAT = 'Y-m-d H:i:s'; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var File + */ + private $driver; + + /** + * @var SplFileInfoFactory + */ + private $splFileInfoFactory; + + /** + * @var ImportFilesInterface + */ + private $importFiles; + + /** + * @var DateTime + */ + private $date; + + /** + * @param File $driver + * @param Filesystem $filesystem + * @param DateTime $date + * @param LoggerInterface $log + * @param SplFileInfoFactory $splFileInfoFactory + * @param GetAssetsByPathsInterface $getAssetsByPaths + * @param ImportFilesInterface $importFiles + */ + public function __construct( + File $driver, + Filesystem $filesystem, + DateTime $date, + LoggerInterface $log, + SplFileInfoFactory $splFileInfoFactory, + GetAssetsByPathsInterface $getAssetsByPaths, + ImportFilesInterface $importFiles + ) { + $this->driver = $driver; + $this->filesystem = $filesystem; + $this->date = $date; + $this->log = $log; + $this->splFileInfoFactory = $splFileInfoFactory; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->importFiles = $importFiles; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + try { + $this->importFiles->execute($this->getPathsToUpdate($paths)); + } catch (LocalizedException $localizedException) { + throw $localizedException; + } catch (\Exception $exception) { + $this->log->critical($exception); + throw new LocalizedException( + __( + 'Could not import media assets for files: %files', + [ + 'files' => implode(', ', $paths) + ] + ) + ); + } + } + + /** + * Return existing assets from files + * + * @param string[] $paths + * @return array + * @throws LocalizedException + */ + private function getPathsToUpdate(array $paths): array + { + $assetPaths = []; + + foreach ($paths as $path) { + $assetPath = $this->getAssetPath($path); + $assetPaths[$assetPath] = $assetPath; + } + + $assets = $this->getAssetsByPaths->execute($assetPaths); + + foreach ($assets as $asset) { + if ($asset->getUpdatedAt() === $this->getFileModificationTime($asset->getPath())) { + unset($assetPaths[$asset->getPath()]); + } + } + + return $assetPaths; + } + + /** + * Retrieve formatted file modification time + * + * @param string $path + * @return string + */ + private function getFileModificationTime(string $path): string + { + return $this->date->gmtDate( + self::DATE_FORMAT, + $this->splFileInfoFactory->create($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() + ); + } + + /** + * Get correct path for media asset + * + * @param string $path + * @return string + */ + private function getAssetPath(string $path): string + { + return $this->driver->getParentDirectory($path) === '.' ? '/' . $path : $path; + } + + /** + * Retrieve media directory instance + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php b/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php new file mode 100644 index 0000000000000..9583c91184d1a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Plugin; + +use Magento\Framework\App\Config\Value; +use Magento\MediaGallerySynchronization\Model\Publish; + +/** + * Plugin to synchronize media storage and media assets database records when media gallery enabled in configuration. + */ +class MediaGallerySyncTrigger +{ + private const MEDIA_GALLERY_CONFIG_VALUE = 'system/media_gallery/enabled'; + private const MEDIA_GALLERY_ENABLED_VALUE = 1; + + /** + * @var Publish + */ + private $publish; + + /** + * @param Publish $publish + */ + public function __construct(Publish $publish) + { + $this->publish = $publish; + } + + /** + * Update media gallery grid table when configuration is saved and media gallery enabled. + * + * @param Value $config + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Value $config, Value $result): Value + { + if ($result->getPath() === self::MEDIA_GALLERY_CONFIG_VALUE + && $result->isValueChanged() + && (int) $result->getValue() === self::MEDIA_GALLERY_ENABLED_VALUE + ) { + $this->publish->execute(); + } + + return $result; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/README.md b/app/code/Magento/MediaGallerySynchronization/README.md new file mode 100644 index 0000000000000..4947c18986f3b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaGallerySynchronization module + +The Magento_MediaGallerySynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallerySynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronization module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php new file mode 100644 index 0000000000000..ad033f35df40b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Test\Integration\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGallerySynchronizationApi\Model\GetContentHashInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetContentHashInterface. + */ +class GetContentHashTest extends TestCase +{ + /** + * @var GetContentHashInterface + */ + private $getContentHash; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->getContentHash = Bootstrap::getObjectManager()->get(GetContentHashInterface::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + } + + /** + * Test for GetContentHashInterface::execute + * + * @dataProvider filesProvider + * @param string $firstFile + * @param string $secondFile + * @param bool $isEqual + * @throws FileSystemException + */ + public function testExecute( + string $firstFile, + string $secondFile, + bool $isEqual + ): void { + $firstHash = $this->getContentHash->execute($this->getImageContent($firstFile)); + $secondHash = $this->getContentHash->execute($this->getImageContent($secondFile)); + $isEqual ? $this->assertEquals($firstHash, $secondHash) : $this->assertNotEquals($firstHash, $secondHash); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'magento.jpg', + 'magento_2.jpg', + true + ], + [ + 'magento.jpg', + 'magento_3.png', + false + ] + ]; + } + + /** + * Get image file content. + * + * @param string $filename + * @return string + * @throws FileSystemException + */ + private function getImageContent(string $filename): string + { + return $this->driver->fileGetContents($this->getImageFilePath($filename)); + } + + /** + * Return image file path + * + * @param string $filename + * @return string + */ + private function getImageFilePath(string $filename): string + { + return dirname(__DIR__, 1) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + $filename + ] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c377daf8fb0b390d89aa4f6f111715fc9118ca1b GIT binary patch literal 55303 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 Hh5vs5{BvNq literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c377daf8fb0b390d89aa4f6f111715fc9118ca1b GIT binary patch literal 55303 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 Hh5vs5{BvNq literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png new file mode 100644 index 0000000000000000000000000000000000000000..366b1b8b9c3f79cedfa18c138d67af7759633e21 GIT binary patch literal 116576 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%0D7F26qBpfBJALO;C_0myLhV+W6@va^)ZR$*3O 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 MC%<-tM}_TOVwK<2wPZ@n-qzc@B6E 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 literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json new file mode 100644 index 0000000000000..e1d4962366978 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-media-gallery-synchronization", + "description": "Magento module provides implementation of the media gallery data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/framework-message-queue": "*", + "magento/module-media-gallery-metadata-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/etc/communication.xml b/app/code/Magento/MediaGallerySynchronization/etc/communication.xml new file mode 100644 index 0000000000000..ba5ae5fc9f9bc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/communication.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="media.gallery.synchronization" is_synchronous="false" request="string[]"> + <handler name="media.gallery.synchronization.handler" + type="Magento\MediaGallerySynchronization\Model\Consume" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml new file mode 100644 index 0000000000000..c19d53deb1b5f --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaGallerySynchronization\Model\Synchronize"/> + <preference for="Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface" type="Magento\MediaGallerySynchronization\Model\FetchBatches"/> + <preference for="Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface" type="Magento\MediaGallerySynchronization\Model\SynchronizeFiles"/> + <preference for="Magento\MediaGallerySynchronizationApi\Model\GetContentHashInterface" type="Magento\MediaGallerySynchronization\Model\GetContentHash"/> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <arguments> + <argument name="importers" xsi:type="array"> + <item name="0" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportMediaAsset</item> + <item name="1" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportImageFileKeywords</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_gallery_asset_synchronizer" xsi:type="object">Magento\MediaGallerySynchronization\Model\SynchronizeFiles</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\FetchMediaStorageFileBatches"> + <arguments> + <argument name="batchSize" xsi:type="number">100</argument> + <argument name="fileExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\FetchBatches"> + <arguments> + <argument name="pageSize" xsi:type="number">100</argument> + </arguments> + </type> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="mediaGallerySynchronization" xsi:type="object">Magento\MediaGallerySynchronization\Console\Command\Synchronize</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Config\Value"> + <plugin name="admin_system_config_adobe_stock_save_plugin" type="Magento\MediaGallerySynchronization\Plugin\MediaGallerySyncTrigger"/> + </type> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/module.xml b/app/code/Magento/MediaGallerySynchronization/etc/module.xml new file mode 100644 index 0000000000000..496f6aa0233a5 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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_MediaGallerySynchronization" /> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..4471d68fd8c47 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="media.gallery.synchronization" queue="media.gallery.synchronization" + connection="db" handler="Magento\MediaGallerySynchronization\Model\Consume::execute"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..1a7cb04847c4a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> + <publisher topic="media.gallery.synchronization"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..81baefbfc53dc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="MediaGallerySynchronization" topic="media.gallery.synchronization" + destinationType="queue" destination="media.gallery.synchronization"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/registration.php b/app/code/Magento/MediaGallerySynchronization/registration.php new file mode 100644 index 0000000000000..9e5f42b14c985 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGallerySynchronization', + __DIR__ +); diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php new file mode 100644 index 0000000000000..de5b00f99e059 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Synchronize assets from the provided files information to database + */ +interface SynchronizeFilesInterface +{ + /** + * Create media gallery assets based on files information and save them to database + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php new file mode 100644 index 0000000000000..0b49780bd7590 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Api; + +/** + * Synchronize assets from the media storage to database + */ +interface SynchronizeInterface +{ + /** + * Synchronize assets from the media storage to database + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php new file mode 100644 index 0000000000000..42cd8265d5087 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +/** + * Fetch data from database in batches + */ +interface FetchBatchesInterface +{ + /** + * Fetch the columns from the database table in batches + * $modificationDateColumn contains the entities which were changed since last execution + * to avoid fetching items that have been previously synchronized + * + * @param string $tableName + * @param array $columns + * @param string|null $modificationDateColumn + * @return \Traversable + */ + public function execute(string $tableName, array $columns, ?string $modificationDateColumn): \Traversable; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/GetContentHashInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/GetContentHashInterface.php new file mode 100644 index 0000000000000..8ca56d8e779d1 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/GetContentHashInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +/** + * Get hashed value of image content. + */ +interface GetContentHashInterface +{ + /** + * Get hashed value of image content. + * + * @param string $content + * @return string + */ + public function execute(string $content): string; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php new file mode 100644 index 0000000000000..8e5df842d8a55 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +/** + * File save pool + */ +class ImportFilesComposite implements ImportFilesInterface +{ + /** + * @var ImportFilesInterface[] + */ + private $importers; + + /** + * @param ImportFilesInterface[] $importers + */ + public function __construct(array $importers) + { + ksort($importers); + $this->importers = $importers; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + foreach ($this->importers as $importer) { + $importer->execute($paths); + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php new file mode 100644 index 0000000000000..40e5947d3a11d --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Save media files data + */ +interface ImportFilesInterface +{ + /** + * Save media files data + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php new file mode 100644 index 0000000000000..1294a4f7679f1 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; + +/** + * A pool of Media storage to database synchronizers + * @see SynchronizeFilesInterface + */ +class SynchronizerPool +{ + /** + * Media storage to database synchronizers + * + * @var SynchronizeFilesInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeFilesInterface[] $synchronizers + */ + public function __construct(array $synchronizers = []) + { + foreach ($synchronizers as $name => $synchronizer) { + if (!$synchronizer instanceof SynchronizeFilesInterface) { + throw new \InvalidArgumentException(sprintf( + 'Synchronizer %s must implement %s.', + $name, + SynchronizeFilesInterface::class + )); + } + } + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeFilesInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/README.md b/app/code/Magento/MediaGallerySynchronizationApi/README.md new file mode 100644 index 0000000000000..1a12883413920 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGallerySynchronizationApi module + +The Magento_MediaGallerySynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallerySynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronizationApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json new file mode 100644 index 0000000000000..427bd2bd4aca7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-synchronization-api", + "description": "Magento module responsible for the media gallery synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml b/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..5cf3b424539bd --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface" type="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml b/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..48736124400c9 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/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_MediaGallerySynchronizationApi" /> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/registration.php b/app/code/Magento/MediaGallerySynchronizationApi/registration.php new file mode 100644 index 0000000000000..542a46d02dd33 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGallerySynchronizationApi', + __DIR__ +); From 1c7318324a5ba6672bdfaf31ebc3f51033cf16ae Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov <vzabaznov@magento.com> Date: Mon, 3 Aug 2020 13:47:18 -0500 Subject: [PATCH 54/65] MC-35903: Refactor place where lock mechanism is used regarding proper lock description --- .../Console/StartConsumerCommand.php | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php index f0f9cf4b68bdb..8ea6290a2a430 100644 --- a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php +++ b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php @@ -79,20 +79,17 @@ protected function execute(InputInterface $input, OutputInterface $output) $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); - try { - if ($singleThread && !$this->lockManager->lock(md5($consumerName),0)) { //phpcs:ignore - $output->writeln('<error>Consumer with the same name is running</error>'); - return \Magento\Framework\Console\Cli::RETURN_FAILURE; - } - - $this->appState->setAreaCode($areaCode ?? 'global'); - - $consumer = $this->consumerFactory->get($consumerName, $batchSize); - $consumer->process($numberOfMessages); - } finally { - if ($singleThread) { - $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore - } + if ($singleThread && !$this->lockManager->lock(md5($consumerName),0)) { //phpcs:ignore + $output->writeln('<error>Consumer with the same name is running</error>'); + return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + + $this->appState->setAreaCode($areaCode ?? 'global'); + + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); + if ($singleThread) { + $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore } return \Magento\Framework\Console\Cli::RETURN_SUCCESS; From 0c6de1dc4578ded5ad22746a0b4b4e53898573c2 Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov <vzabaznov@magento.com> Date: Mon, 3 Aug 2020 17:07:10 -0500 Subject: [PATCH 55/65] MC-35903: Refactor place where lock mechanism is used regarding proper lock description --- .../MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php index 274386a9bb685..1aa805f0e323b 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php @@ -187,7 +187,7 @@ public function executeDataProvider() 'singleThread' => true, 'lockExpects' => 1, 'isLocked' => false, - 'unlockExpects' => 1, + 'unlockExpects' => 0, 'runProcessExpects' => 0, 'expectedReturn' => Cli::RETURN_FAILURE, ], From 22112976b09ae8552accecfe55d40ae66c651089 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 4 Aug 2020 13:00:10 +0100 Subject: [PATCH 56/65] Fixes accroding to code review comments --- .../Listing/Columns/SourceIconProvider.php | 2 +- .../Listing/Filters/Options/Store.php | 2 +- .../deleteImageWithDetailConfirmation.js | 4 ++-- .../view/adminhtml/web/js/image-uploader.js | 21 +++++++++---------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php index aec99bf7d3b8c..e425c9488b5c2 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php @@ -84,7 +84,7 @@ public function prepareDataSource(array $dataSource): array * * @return string|null */ - public function getSourceIconUrl(string $sourceName): ?string + private function getSourceIconUrl(string $sourceName): ?string { return isset($this->sourceIcons[$sourceName]) ? $this->assetRepository->getUrlWithParams( diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php index cf49377c19837..f60124a6cf933 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php @@ -17,7 +17,7 @@ class Store extends StoreOptions /** * All Store Views value */ - const ALL_STORE_VIEWS = '0'; + private const ALL_STORE_VIEWS = '0'; /** * Get options diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js index 51ba2a258faf1..fb7bb0f1b0d0c 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -22,8 +22,8 @@ define([ */ deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { var imagesCount = Object.keys(recordsIds).length, - confirmationContent = $t('%1 Are you sure you want to delete "%2" image%3?') - .replace('%2', Object.keys(recordsIds).length).replace('%3', imagesCount > 1 ? 's' : ''), + confirmationContent = $t('%1 Are you sure you want to delete "%2" image(s)?') + .replace('%2', Object.keys(recordsIds).length), deferred = $.Deferred(); getDetails(imageDetailsUrl, recordsIds) diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js index 3b69ca07f5771..58fff640f9db3 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js @@ -85,13 +85,17 @@ define([ add: function (e, data) { if (!this.isSizeExceeded(data.files[0]).passed) { - this.addValidationErrorMessage('Cannot upload "' + data.files[0].name + - '". File exceeds maximum file size limit.'); + this.addValidationErrorMessage( + $t('Cannot upload "%1". File exceeds maximum file size limit.') + .replace('%1', data.files[0].name) + ); return; } else if (!this.isFileNameLengthExceeded(data.files[0]).passed) { - this.addValidationErrorMessage('Cannot upload "' + data.files[0].name + - '". Filename is too long, must be 90 characters or less.'); + this.addValidationErrorMessage( + $t('Cannot upload "%1". Filename is too long, must be 90 characters or less.') + .replace('%1', data.files[0].name) + ); return; } @@ -137,10 +141,7 @@ define([ * @param {String} message */ addValidationErrorMessage: function (message) { - this.mediaGridMessages().add( - 'error', - $t(message) - ); + this.mediaGridMessages().add('error', message); this.count() < 2 || this.mediaGridMessages().scheduleCleanup(); }, @@ -203,12 +204,10 @@ define([ * Show success message, and files counts */ showSuccessMessage: function () { - var prefix = this.count() === 1 ? 'an image' : this.count() + ' images'; - this.mediaGridMessages().messages.remove(function (item) { return item.code === 'success'; }); - this.mediaGridMessages().add('success', $t('Successfully uploaded ' + prefix)); + this.mediaGridMessages().add('success', $t('Assets have been successfully uploaded!')); this.count(this.count() + 1); }, From b326cc14301db6366d0dc1d5a35b69c8c93820e6 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 4 Aug 2020 13:04:21 +0100 Subject: [PATCH 57/65] Removed renditions modules --- .../MediaGalleryRenditions/LICENSE.txt | 48 -------------- .../MediaGalleryRenditions/LICENSE_AFL.txt | 48 -------------- .../Magento/MediaGalleryRenditions/README.md | 13 ---- .../MediaGalleryRenditions/composer.json | 21 ------- .../etc/adminhtml/system.xml | 24 ------- .../MediaGalleryRenditions/etc/config.xml | 17 ----- .../MediaGalleryRenditions/etc/module.xml | 10 --- .../MediaGalleryRenditions/registration.php | 14 ----- .../MediaGalleryRenditionsApi/LICENSE.txt | 48 -------------- .../MediaGalleryRenditionsApi/LICENSE_AFL.txt | 48 -------------- .../Model/Config.php | 62 ------------------- .../MediaGalleryRenditionsApi/README.md | 13 ---- .../MediaGalleryRenditionsApi/composer.json | 21 ------- .../MediaGalleryRenditionsApi/etc/module.xml | 10 --- .../registration.php | 14 ----- 15 files changed, 411 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE.txt delete mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt delete mode 100644 app/code/Magento/MediaGalleryRenditions/README.md delete mode 100644 app/code/Magento/MediaGalleryRenditions/composer.json delete mode 100644 app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml delete mode 100644 app/code/Magento/MediaGalleryRenditions/etc/config.xml delete mode 100644 app/code/Magento/MediaGalleryRenditions/etc/module.xml delete mode 100644 app/code/Magento/MediaGalleryRenditions/registration.php delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/README.md delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/composer.json delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml delete mode 100644 app/code/Magento/MediaGalleryRenditionsApi/registration.php diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt deleted file mode 100644 index 36b2459f6aa63..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Open Software License ("OSL") v. 3.0 - -This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Open Software License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt deleted file mode 100644 index f39d641b18a19..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Academic Free License ("AFL") v. 3.0 - -This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Academic Free License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md deleted file mode 100644 index df856e8003a84..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Magento_MediaGalleryRenditions module - -The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items. - -## Extensibility - -Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). - -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. - -## Additional information - -For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json deleted file mode 100644 index 50b18752fc506..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/composer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "magento/module-media-gallery-renditions", - "description": "Magento module that implements height and width fields for for media gallery items.", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*" - }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "autoload": { - "files": [ - "registration.php" - ], - "psr-4": { - "Magento\\MediaGalleryRenditions\\": "" - } - } -} diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml deleted file mode 100644 index f23f94f186f68..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml +++ /dev/null @@ -1,24 +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="media_gallery_renditions" translate="label" type="text" sortOrder="1010" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Media Gallery Renditions</label> - <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Width</label> - <validate>validate-zero-or-greater validate-digits</validate> - </field> - <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Height</label> - <validate>validate-zero-or-greater validate-digits</validate> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml deleted file mode 100644 index 58c5aa1f11fd2..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/etc/config.xml +++ /dev/null @@ -1,17 +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_Store:etc/config.xsd"> - <default> - <system> - <media_gallery_renditions> - <width>1000</width> - <height>1000</height> - </media_gallery_renditions> - </system> - </default> -</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml deleted file mode 100644 index 792a9e128cc40..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/etc/module.xml +++ /dev/null @@ -1,10 +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:Module/etc/module.xsd"> - <module name="Magento_MediaGalleryRenditions" /> -</config> diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php deleted file mode 100644 index 275c06f752a63..0000000000000 --- a/app/code/Magento/MediaGalleryRenditions/registration.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Framework\Component\ComponentRegistrar; - -ComponentRegistrar::register( - ComponentRegistrar::MODULE, - 'Magento_MediaGalleryRenditions', - __DIR__ -); diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt deleted file mode 100644 index 36b2459f6aa63..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Open Software License ("OSL") v. 3.0 - -This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Open Software License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt deleted file mode 100644 index f39d641b18a19..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Academic Free License ("AFL") v. 3.0 - -This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Academic Free License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php b/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php deleted file mode 100644 index e558f23ab9608..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\MediaGalleryRenditionsApi\Model; - -use Magento\Framework\App\Config\ScopeConfigInterface; - -/** - * Class responsible for providing access to Media Gallery Renditions system configuration. - */ -class Config -{ - /** - * Config path for Media Gallery Renditions Width - */ - private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; - - /** - * Config path for Media Gallery Renditions Height - */ - private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; - - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - - /** - * Config constructor. - * @param ScopeConfigInterface $scopeConfig - */ - public function __construct( - ScopeConfigInterface $scopeConfig - ) { - $this->scopeConfig = $scopeConfig; - } - - /** - * Get max width - * - * @return int - */ - public function getWidth(): int - { - return $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); - } - - /** - * Get max height - * - * @return int - */ - public function getHeight(): int - { - return $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); - } -} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md deleted file mode 100644 index 42478c0c9b520..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Magento_MediaGalleryRenditionsApi module - -The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions. - -## Extensibility - -Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). - -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module. - -## Additional information - -For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json deleted file mode 100644 index 6e3c559f001c1..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/composer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "magento/module-media-gallery-renditions-api", - "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*" - }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "autoload": { - "files": [ - "registration.php" - ], - "psr-4": { - "Magento\\MediaGalleryRenditionsApi\\": "" - } - } -} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml deleted file mode 100644 index 64efa325ec791..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml +++ /dev/null @@ -1,10 +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:Module/etc/module.xsd"> - <module name="Magento_MediaGalleryRenditionsApi" /> -</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php deleted file mode 100644 index bf057f2d2adbf..0000000000000 --- a/app/code/Magento/MediaGalleryRenditionsApi/registration.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Framework\Component\ComponentRegistrar; - -ComponentRegistrar::register( - ComponentRegistrar::MODULE, - 'Magento_MediaGalleryRenditionsApi', - __DIR__ -); From a9a508d811d29488c45740672571ed1dba5c0106 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 4 Aug 2020 16:07:57 +0100 Subject: [PATCH 58/65] Fixed static tests --- .../web/js/action/deleteImageWithDetailConfirmation.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js index fb7bb0f1b0d0c..51d124ca319e6 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -21,8 +21,7 @@ define([ * @param {String} deleteImageUrl */ deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { - var imagesCount = Object.keys(recordsIds).length, - confirmationContent = $t('%1 Are you sure you want to delete "%2" image(s)?') + var confirmationContent = $t('%1 Are you sure you want to delete "%2" image(s)?') .replace('%2', Object.keys(recordsIds).length), deferred = $.Deferred(); From 056cb380f6d9bdcf7e68a5abde0eb6c52eaca1d2 Mon Sep 17 00:00:00 2001 From: dnyomo <dnyomo@briteskies.com> Date: Tue, 4 Aug 2020 13:15:24 -0400 Subject: [PATCH 59/65] Code style fixes --- .../DataProvider/Product/SearchCriteriaBuilder.php | 4 +++- .../Resolver/Products/DataProvider/ProductSearch.php | 7 +++---- .../Model/Resolver/Products/Query/Search.php | 9 ++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index fe7272b933f75..b6837b334fdd8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -213,7 +213,9 @@ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, ar } else { $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; if ($categoryIdFilter) { - if (!is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) || count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1) { + if (!is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) + || count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1 + ) { $defaultSortOrder[] = $this->sortOrderBuilder ->setField(EavAttributeInterface::POSITION) ->setDirection(SortOrder::SORT_ASC) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index e4823b4aa557a..c2ee0cc89468a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -87,15 +87,14 @@ public function __construct( * * @param SearchCriteriaInterface $searchCriteria * @param SearchResultInterface $searchResult - * @param array $args * @param array $attributes * @param ContextInterface|null $context * @return SearchResultsInterface + * @throws InputException */ public function getList( SearchCriteriaInterface $searchCriteria, SearchResultInterface $searchResult, - array $args, array $attributes = [], ContextInterface $context = null ): SearchResultsInterface { @@ -108,7 +107,7 @@ public function getList( $this->getSearchResultsApplier( $searchResult, $collection, - $this->getSortOrderArray($searchCriteriaForCollection, $args) + $this->getSortOrderArray($searchCriteriaForCollection) )->apply(); $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); @@ -154,7 +153,7 @@ private function getSearchResultsApplier( * @return array * @throws InputException */ - private function getSortOrderArray(SearchCriteriaInterface $searchCriteria, $args) + private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) { $ordersArray = []; $sortOrders = $searchCriteria->getSortOrders(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index faf1d56452a24..4eb76fb5c2d5b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -12,6 +12,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; @@ -83,6 +84,7 @@ public function __construct( * @param ResolveInfo $info * @param ContextInterface $context * @return SearchResult + * @throws InputException */ public function getResult( array $args, @@ -103,7 +105,12 @@ public function getResult( //Address limitations of sort and pagination on search API apply original pagination from GQL query $searchCriteria->setPageSize($realPageSize); $searchCriteria->setCurrentPage($realCurrentPage); - $searchResults = $this->productsProvider->getList($searchCriteria, $itemsResults, $args, $queryFields, $context); + $searchResults = $this->productsProvider->getList( + $searchCriteria, + $itemsResults, + $queryFields, + $context + ); $totalPages = $realPageSize ? ((int)ceil($searchResults->getTotalCount() / $realPageSize)) : 0; From 2f1b09c3eaa64acf0b6688d2e92edb44d5ed244b Mon Sep 17 00:00:00 2001 From: dnyomo <dnyomo@briteskies.com> Date: Tue, 4 Aug 2020 14:05:40 -0400 Subject: [PATCH 60/65] Code style fixes --- .../Model/Resolver/Products/DataProvider/ProductSearch.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index c2ee0cc89468a..071c0786a9bfc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -7,7 +7,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; -use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -19,7 +18,6 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; -use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\InputException; use Magento\GraphQl\Model\Query\ContextInterface; @@ -149,7 +147,6 @@ private function getSearchResultsApplier( * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...] * * @param SearchCriteriaInterface $searchCriteria - * @param array $args * @return array * @throws InputException */ From e1f12471d36cdd94165d2023f34e060de49eba83 Mon Sep 17 00:00:00 2001 From: dnyomo <dnyomo@briteskies.com> Date: Tue, 4 Aug 2020 15:03:29 -0400 Subject: [PATCH 61/65] Code style fixes and added a message around converting _id to entity_id --- .../Model/Resolver/Products/DataProvider/ProductSearch.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 071c0786a9bfc..66d89f050899c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -18,11 +18,11 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; -use Magento\Framework\Exception\InputException; use Magento\GraphQl\Model\Query\ContextInterface; /** * Product field data provider for product search, used for GraphQL resolver processing. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductSearch { @@ -88,7 +88,6 @@ public function __construct( * @param array $attributes * @param ContextInterface|null $context * @return SearchResultsInterface - * @throws InputException */ public function getList( SearchCriteriaInterface $searchCriteria, @@ -148,7 +147,6 @@ private function getSearchResultsApplier( * * @param SearchCriteriaInterface $searchCriteria * @return array - * @throws InputException */ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) { @@ -156,6 +154,8 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) $sortOrders = $searchCriteria->getSortOrders(); if (is_array($sortOrders)) { foreach ($sortOrders as $sortOrder) { + // I am replacing _id with entity_id because in ElasticSearch _id is required for sorting by ID. + // Where as entity_id is required when using ID as the sort in $collection->load();. if ($sortOrder->getField() === '_id') { $sortOrder->setField('entity_id'); } From a50dae1c2090bd8ed17967c3ff6565c0f94ca933 Mon Sep 17 00:00:00 2001 From: dnyomo <dnyomo@briteskies.com> Date: Tue, 4 Aug 2020 15:11:22 -0400 Subject: [PATCH 62/65] Updated the message with the related method --- .../Model/Resolver/Products/DataProvider/ProductSearch.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 66d89f050899c..d8b46beb6ba2b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -156,6 +156,7 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) foreach ($sortOrders as $sortOrder) { // I am replacing _id with entity_id because in ElasticSearch _id is required for sorting by ID. // Where as entity_id is required when using ID as the sort in $collection->load();. + // @see \Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search::getResult if ($sortOrder->getField() === '_id') { $sortOrder->setField('entity_id'); } From 1c340c33735218f98840b025b92e39a77e0f53e5 Mon Sep 17 00:00:00 2001 From: dnyomo <dnyomo@briteskies.com> Date: Tue, 4 Aug 2020 15:15:07 -0400 Subject: [PATCH 63/65] Remvoed @SuppressWarnings(PHPMD.CouplingBetweenObjects) --- .../Model/Resolver/Products/DataProvider/ProductSearch.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index d8b46beb6ba2b..4807cad54bd50 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -22,7 +22,6 @@ /** * Product field data provider for product search, used for GraphQL resolver processing. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductSearch { From 201b67bec993a1faa359e31f7212b43980d5cc1e Mon Sep 17 00:00:00 2001 From: Valerii Naida <vnayda@adobe.com> Date: Tue, 4 Aug 2020 21:48:07 -0500 Subject: [PATCH 64/65] magento/magento2-login-as-customer#144: "Login as Customer" functionality should be enabled by default. --- app/code/Magento/LoginAsCustomer/composer.json | 1 + .../Controller/Adminhtml/Login/Login.php | 3 ++- .../Ui/Customer/Component/Button/DataProvider.php | 2 -- .../IsLoginAsCustomerEnabledForCustomerResultInterface.php | 2 -- .../Api/IsLoginAsCustomerEnabledForCustomerInterface.php | 2 -- .../CustomerData/LoginAsCustomerUi.php | 3 ++- .../Model/AuthenticateCustomerBySecret.php | 3 ++- .../LoginAsCustomerFrontendUi/ViewModel/Configuration.php | 3 ++- app/code/Magento/LoginAsCustomerPageCache/composer.json | 3 ++- app/code/Magento/LoginAsCustomerSales/composer.json | 3 ++- 10 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/composer.json b/app/code/Magento/LoginAsCustomer/composer.json index ec81374528e7b..e58ec90e8f8bb 100755 --- a/app/code/Magento/LoginAsCustomer/composer.json +++ b/app/code/Magento/LoginAsCustomer/composer.json @@ -4,6 +4,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/module-backend": "*", "magento/module-customer": "*", "magento/module-login-as-customer-api": "*" }, diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 1c2f0cbaa197c..39a7055ed65bb 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -131,7 +131,8 @@ public function __construct( $this->saveAuthenticationData = $saveAuthenticationData; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; - $this->setLoggedAsCustomerCustomerId = $setLoggedAsCustomerCustomerId ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerCustomerIdInterface::class); + $this->setLoggedAsCustomerCustomerId = $setLoggedAsCustomerCustomerId + ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerCustomerIdInterface::class); $this->isLoginAsCustomerEnabled = $isLoginAsCustomerEnabled ?? ObjectManager::getInstance()->get(IsLoginAsCustomerEnabledForCustomerInterface::class); } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php index 19db8c67f945e..24a70fc429467 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php @@ -14,8 +14,6 @@ * Get data for Login as Customer button. * * Use this class as a base for virtual types declaration. - * - * @api */ class DataProvider { diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php index 4f517fd7f315f..b7d3a616176ef 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php @@ -9,8 +9,6 @@ /** * IsLoginAsCustomerEnabledForCustomerInterface results. - * - * @api */ interface IsLoginAsCustomerEnabledForCustomerResultInterface { diff --git a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php index 7742bc23e421b..a5355fd4566d5 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php @@ -11,8 +11,6 @@ /** * Check if Login as Customer functionality is enabled for Customer. - * - * @api */ interface IsLoginAsCustomerEnabledForCustomerInterface { diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php b/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php index 78ac010c965e3..140f31e3467f1 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php @@ -48,7 +48,8 @@ public function __construct( ) { $this->customerSession = $customerSession; $this->storeManager = $storeManager; - $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); } /** diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php b/app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php index 2903016c36ddf..0604d2546ef79 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php @@ -48,7 +48,8 @@ public function __construct( ) { $this->getAuthenticationDataBySecret = $getAuthenticationDataBySecret; $this->customerSession = $customerSession; - $this->setLoggedAsCustomerAdminId = $setLoggedAsCustomerAdminId ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerAdminIdInterface::class); + $this->setLoggedAsCustomerAdminId = $setLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerAdminIdInterface::class); } /** diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php b/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php index e7a5cd146410e..357ede238585b 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php @@ -45,7 +45,8 @@ public function __construct( ) { $this->config = $config; $this->httpContext = $httpContext; - $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); } /** diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json index 195a08fc19d83..410808f9e79f6 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/composer.json +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -5,7 +5,8 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-customer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { "magento/module-page-cache": "*" diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json index 3965e8acf87d8..e93b40e0440c8 100644 --- a/app/code/Magento/LoginAsCustomerSales/composer.json +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -6,7 +6,8 @@ "magento/framework": "*", "magento/module-backend": "*", "magento/module-customer": "*", - "magento/module-user": "*" + "magento/module-user": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { "magento/module-sales": "*" From 00f5d468c2fd86b1c6ccbb8f79cfa75b009d5976 Mon Sep 17 00:00:00 2001 From: Valerii Naida <vnayda@adobe.com> Date: Tue, 4 Aug 2020 22:33:25 -0500 Subject: [PATCH 65/65] magento/magento2-login-as-customer#144: "Login as Customer" functionality should be enabled by default. --- .../Model/AuthenticateCustomerBySecret.php | 2 +- .../LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php | 5 ++--- .../Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml | 1 - app/code/Magento/LoginAsCustomerPageCache/composer.json | 1 - app/code/Magento/LoginAsCustomerSales/composer.json | 1 - 5 files changed, 3 insertions(+), 7 deletions(-) rename app/code/Magento/{LoginAsCustomerFrontendUi => LoginAsCustomer}/Model/AuthenticateCustomerBySecret.php (97%) diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php similarity index 97% rename from app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php rename to app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php index 0604d2546ef79..808b01bac58aa 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/Model/AuthenticateCustomerBySecret.php +++ b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\LoginAsCustomerFrontendUi\Model; +namespace Magento\LoginAsCustomer\Model; use Magento\Customer\Model\Session; use Magento\Framework\App\ObjectManager; diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php index 3516a181b5dde..c67b0d9dd5273 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php @@ -8,7 +8,6 @@ namespace Magento\LoginAsCustomerAdminUi\Plugin\Button; use Magento\Backend\Block\Widget\Button\ButtonList; -use Magento\Backend\Block\Widget\Button\Toolbar; use Magento\Framework\AuthorizationInterface; use Magento\Framework\Escaper; use Magento\Framework\View\Element\AbstractBlock; @@ -62,13 +61,13 @@ public function __construct( /** * Add Login as Customer button. * - * @param \Magento\Backend\Block\Widget\Button\Toolbar $subject + * @param \Magento\Backend\Block\Widget\Button\ToolbarInterface $subject * @param \Magento\Framework\View\Element\AbstractBlock $context * @param \Magento\Backend\Block\Widget\Button\ButtonList $buttonList * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforePushButtons( - Toolbar $subject, + \Magento\Backend\Block\Widget\Button\ToolbarInterface $subject, AbstractBlock $context, ButtonList $buttonList ): void { diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml index e42d33383ac42..bff511b6bb6e6 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml @@ -6,7 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface" type="Magento\LoginAsCustomerFrontendUi\Model\AuthenticateCustomerBySecret"/> <type name="Magento\Customer\CustomerData\SectionPoolInterface"> <arguments> <argument name="sectionSourceMap" xsi:type="array"> diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json index 410808f9e79f6..84d7f2e2a6730 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/composer.json +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -4,7 +4,6 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", - "magento/module-customer": "*", "magento/module-store": "*", "magento/module-login-as-customer-api": "*" }, diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json index e93b40e0440c8..3891504e54092 100644 --- a/app/code/Magento/LoginAsCustomerSales/composer.json +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -5,7 +5,6 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-backend": "*", - "magento/module-customer": "*", "magento/module-user": "*", "magento/module-login-as-customer-api": "*" },