From cc08f74995dd54cc3f2c20474211448feb550e04 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina Date: Mon, 4 Mar 2019 13:20:08 +0300 Subject: [PATCH 01/78] MAGETWO-71835: [Product grid] SC's values aren't sorted alphabetically in the tooltip - Sort values case insensitive --- .../Magento/Ui/view/base/web/js/grid/columns/expandable.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js index 8bbe5971490a7..9d7477eb8ae21 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js @@ -67,7 +67,11 @@ define([ } }); - return labels.sort(); + return labels.sort( + function(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + } + ); }, /** From 043f16154a27798c7f74e8429c7121ae04797417 Mon Sep 17 00:00:00 2001 From: vprohorov Date: Fri, 29 Mar 2019 19:17:12 +0300 Subject: [PATCH 02/78] MAGETWO-91589: Slow query delete on sub SELECT query - Query rewrite --- .../Model/ResourceModel/Category/Product.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php index 311cc6de76114..983fa30c98c7b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php @@ -86,7 +86,27 @@ public function removeMultiple(array $removeData) */ public function removeMultipleByProductCategory(array $filter) { - return $this->getConnection()->deleteFromSelect($this->prepareSelect($filter), self::TABLE_NAME); + return $this->getConnection()->deleteFromSelect($this->prepareJoin($filter), self::TABLE_NAME); + } + + /** + * Prepare select statement for specific filter + * + * @param array $data + * @return \Magento\Framework\DB\Select + */ + private function prepareJoin($data) + { + $select = $this->getConnection()->select(); + $select->from(DbStorage::TABLE_NAME); + $select->join( + self::TABLE_NAME, + DbStorage::TABLE_NAME . '.url_rewrite_id = ' . self::TABLE_NAME . '.url_rewrite_id' + ); + foreach ($data as $column => $value) { + $select->where(DbStorage::TABLE_NAME . '.' . $column . ' IN (?)', $value); + } + return $select; } /** From 72b03df3d05e47ebdea737106093fb0733682767 Mon Sep 17 00:00:00 2001 From: vprohorov Date: Mon, 1 Apr 2019 16:41:01 +0300 Subject: [PATCH 03/78] MAGETWO-91589: Slow query delete on sub SELECT query - Fix static tests --- .../Model/ResourceModel/Category/Product.php | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php index 983fa30c98c7b..a475e3d5f4b82 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\CatalogUrlRewrite\Model\ResourceModel\Category; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; @@ -49,7 +52,7 @@ protected function _construct() public function saveMultiple(array $insertData) { $connection = $this->getConnection(); - if (sizeof($insertData) <= self::CHUNK_SIZE) { + if (count($insertData) <= self::CHUNK_SIZE) { return $connection->insertMultiple($this->getTable(self::TABLE_NAME), $insertData); } $data = array_chunk($insertData, self::CHUNK_SIZE); @@ -86,7 +89,7 @@ public function removeMultiple(array $removeData) */ public function removeMultipleByProductCategory(array $filter) { - return $this->getConnection()->deleteFromSelect($this->prepareJoin($filter), self::TABLE_NAME); + return $this->getConnection()->deleteFromSelect($this->prepareSelect($filter), self::TABLE_NAME); } /** @@ -95,7 +98,7 @@ public function removeMultipleByProductCategory(array $filter) * @param array $data * @return \Magento\Framework\DB\Select */ - private function prepareJoin($data) + private function prepareSelect($data) { $select = $this->getConnection()->select(); $select->from(DbStorage::TABLE_NAME); @@ -108,21 +111,4 @@ private function prepareJoin($data) } return $select; } - - /** - * Prepare select statement for specific filter - * - * @param array $data - * @return \Magento\Framework\DB\Select - */ - private function prepareSelect($data) - { - $select = $this->getConnection()->select(); - $select->from($this->getTable(DbStorage::TABLE_NAME), 'url_rewrite_id'); - - foreach ($data as $column => $value) { - $select->where($this->getConnection()->quoteIdentifier($column) . ' IN (?)', $value); - } - return $select; - } } From e66b7ed0452b3fbeee31ce3269d1f0ce33620a12 Mon Sep 17 00:00:00 2001 From: vprohorov Date: Wed, 3 Apr 2019 19:12:36 +0300 Subject: [PATCH 04/78] MAGETWO-63599: [GitHub] catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes #8204 - Added checks for zero byte files --- .../Image/Adapter/AbstractAdapter.php | 21 ++++++++-- .../Magento/Framework/Image/Adapter/Gd2.php | 39 +++++++++++-------- .../Image/Test/Unit/Adapter/Gd2Test.php | 10 +++++ .../Unit/Adapter/_files/global_php_mock.php | 13 +++++++ 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php index 6042e4eee491d..00f2c39aea8a1 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php +++ b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\Image\Adapter; use Magento\Framework\App\Filesystem\DirectoryList; /** + * Image abstract adapter + * * @file Abstract.php * @author Magento Core Team * @SuppressWarnings(PHPMD.TooManyFields) @@ -169,6 +174,7 @@ abstract public function open($fileName); /** * Save image to specific path. + * * If some folders of path does not exist they will be created * * @param null|string $destination @@ -301,7 +307,7 @@ public function getImageType() if ($this->_fileType) { return $this->_fileType; } else { - if ($this->_canProcess()) { + if ($this->_canProcess() && filesize($this->_fileName) > 0) { list($this->_imageSrcWidth, $this->_imageSrcHeight, $this->_fileType) = getimagesize($this->_fileName); return $this->_fileType; } @@ -620,7 +626,7 @@ protected function _checkDimensions($frameWidth, $frameHeight) $frameHeight !== null && $frameHeight <= 0 || empty($frameWidth) && empty($frameHeight) ) { - throw new \Exception('Invalid image dimensions.'); + throw new \InvalidArgumentException('Invalid image dimensions.'); } } @@ -687,7 +693,9 @@ protected function _prepareDestination($destination = null, $newName = null) $this->directoryWrite->create($this->directoryWrite->getRelativePath($destination)); } catch (\Magento\Framework\Exception\FileSystemException $e) { $this->logger->critical($e); - throw new \Exception('Unable to write file into directory ' . $destination . '. Access forbidden.'); + throw new \DomainException( + 'Unable to write file into directory ' . $destination . '. Access forbidden.' + ); } } @@ -710,12 +718,19 @@ protected function _canProcess() * @param string $filePath * @return bool * @throws \InvalidArgumentException + * @throws \DomainException + * @throws \BadFunctionCallException + * @throws \RuntimeException + * @throws \OverflowException */ public function validateUploadFile($filePath) { if (!file_exists($filePath)) { throw new \InvalidArgumentException("File '{$filePath}' does not exists."); } + if (filesize($filePath) === 0) { + throw new \InvalidArgumentException('Wrong file size.'); + } if (!getimagesize($filePath)) { throw new \InvalidArgumentException('Disallowed file type.'); } diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index cfba1820bec05..73bbbc1176913 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\Image\Adapter; /** @@ -59,6 +62,9 @@ protected function _reset() */ public function open($filename) { + if (filesize($filename) === 0) { + throw new \InvalidArgumentException("Wrong file size: '{$filename}'."); + } $this->_fileName = $filename; $this->_reset(); $this->getMimeType(); @@ -225,7 +231,8 @@ public function getImage() * @param null|int $fileType * @param string $unsupportedText * @return string - * @throws \Exception + * @throws \InvalidArgumentException + * @throws \BadFunctionCallException */ private function _getCallback($callbackType, $fileType = null, $unsupportedText = 'Unsupported image format.') { @@ -233,10 +240,10 @@ private function _getCallback($callbackType, $fileType = null, $unsupportedText $fileType = $this->_fileType; } if (empty(self::$_callbacks[$fileType])) { - throw new \Exception($unsupportedText); + throw new \InvalidArgumentException($unsupportedText); } if (empty(self::$_callbacks[$fileType][$callbackType])) { - throw new \Exception('Callback not found.'); + throw new \BadFunctionCallException('Callback not found.'); } return self::$_callbacks[$fileType][$callbackType]; } @@ -248,7 +255,7 @@ private function _getCallback($callbackType, $fileType = null, $unsupportedText * * @param resource &$imageResourceTo * @return int - * @throws \Exception + * @throws \InvalidArgumentException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function _fillBackgroundColor(&$imageResourceTo) @@ -261,17 +268,17 @@ private function _fillBackgroundColor(&$imageResourceTo) // fill truecolor png with alpha transparency if ($isAlpha) { if (!imagealphablending($imageResourceTo, false)) { - throw new \Exception('Failed to set alpha blending for PNG image.'); + throw new \InvalidArgumentException('Failed to set alpha blending for PNG image.'); } $transparentAlphaColor = imagecolorallocatealpha($imageResourceTo, 0, 0, 0, 127); if (false === $transparentAlphaColor) { - throw new \Exception('Failed to allocate alpha transparency for PNG image.'); + throw new \InvalidArgumentException('Failed to allocate alpha transparency for PNG image.'); } if (!imagefill($imageResourceTo, 0, 0, $transparentAlphaColor)) { - throw new \Exception('Failed to fill PNG image with alpha transparency.'); + throw new \InvalidArgumentException('Failed to fill PNG image with alpha transparency.'); } if (!imagesavealpha($imageResourceTo, true)) { - throw new \Exception('Failed to save alpha transparency into PNG image.'); + throw new \InvalidArgumentException('Failed to save alpha transparency into PNG image.'); } return $transparentAlphaColor; @@ -283,22 +290,22 @@ private function _fillBackgroundColor(&$imageResourceTo) $transparentColor = imagecolorallocate($imageResourceTo, $r, $g, $b); } if (false === $transparentColor) { - throw new \Exception('Failed to allocate transparent color for image.'); + throw new \InvalidArgumentException('Failed to allocate transparent color for image.'); } if (!imagefill($imageResourceTo, 0, 0, $transparentColor)) { - throw new \Exception('Failed to fill image with transparency.'); + throw new \InvalidArgumentException('Failed to fill image with transparency.'); } imagecolortransparent($imageResourceTo, $transparentColor); return $transparentColor; } } catch (\Exception $e) { - // fallback to default background color + throw new \DomainException('Failed to fill image.'); } } list($r, $g, $b) = $this->_backgroundColor; $color = imagecolorallocate($imageResourceTo, $r, $g, $b); if (!imagefill($imageResourceTo, 0, 0, $color)) { - throw new \Exception("Failed to fill image background with color {$r} {$g} {$b}."); + throw new \InvalidArgumentException("Failed to fill image background with color {$r} {$g} {$b}."); } return $color; @@ -637,13 +644,13 @@ public function crop($top = 0, $left = 0, $right = 0, $bottom = 0) * Checks required dependencies * * @return void - * @throws \Exception If some of dependencies are missing + * @throws \RuntimeException If some of dependencies are missing */ public function checkDependencies() { foreach ($this->_requiredExtensions as $value) { if (!extension_loaded($value)) { - throw new \Exception("Required PHP extension '{$value}' was not loaded."); + throw new \RuntimeException("Required PHP extension '{$value}' was not loaded."); } } } @@ -755,7 +762,7 @@ protected function _createImageFromText($text) * @param string $text * @param string $font * @return void - * @throws \Exception + * @throws \InvalidArgumentException */ protected function _createImageFromTtfText($text, $font) { @@ -777,7 +784,7 @@ protected function _createImageFromTtfText($text, $font) $text ); if ($result === false) { - throw new \Exception('Unable to create TTF text'); + throw new \InvalidArgumentException('Unable to create TTF text'); } } diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php index 41281c96e1bb4..ccf52c7c59ceb 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\Image\Test\Unit\Adapter; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -24,6 +27,13 @@ class Gd2Test extends \PHPUnit\Framework\TestCase */ public static $imageData = []; + /** + * Simulation of filesize() function + * + * @var int + */ + public static $imageSize = 1; + /** * Adapter for testing * @var \Magento\Framework\Image\Adapter\Gd2 diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php index 3e90359b97b47..a62909b495ab4 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\Image\Adapter; use Magento\Framework\Image\Test\Unit\Adapter\Gd2Test; @@ -35,6 +38,16 @@ function getimagesize($file) return Gd2Test::$imageData; } +/** + * @param $file + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function filesize($file) +{ + return Gd2Test::$imageSize; +} + /** * @param $real * @return int From b9a96e14338a28aa43909ddb1371718670f7d50d Mon Sep 17 00:00:00 2001 From: vprohorov Date: Thu, 4 Apr 2019 15:35:45 +0300 Subject: [PATCH 05/78] MAGETWO-63599: [GitHub] catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes #8204 - Added checks for zero byte files --- lib/internal/Magento/Framework/Image/Adapter/Gd2.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index 73bbbc1176913..300f7a75ab509 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -declare(strict_types=1); - namespace Magento\Framework\Image\Adapter; /** From 9950da00dcaf4c6a26b240d3500486a5a22624f6 Mon Sep 17 00:00:00 2001 From: Mikalai Shostka Date: Thu, 4 Apr 2019 17:39:05 +0300 Subject: [PATCH 06/78] MAGETWO-93061: CMS page of second website with same URLkey as first website, show content of First website instead of second website content. - Add switch store to url --- .../Page/Grid/Renderer/Action/UrlBuilder.php | 65 +++++++++++++++++-- .../Store/Controller/Store/SwitchAction.php | 11 +++- .../Model/StoreSwitcher/RewriteUrl.php | 27 ++++++-- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php index 08ba2c3fff330..8bff494ef79f3 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Block\Adminhtml\Page\Grid\Renderer\Action; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\App\ActionInterface; +use Magento\Store\Model\StoreManagerInterface; + /** * Url builder class used to compose dynamic urls. */ @@ -15,12 +22,31 @@ class UrlBuilder */ protected $frontendUrlBuilder; + /** + * @var EncoderInterface + */ + private $urlEncoder; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\UrlInterface $frontendUrlBuilder + * @param EncoderInterface|null $urlEncoder + * @param StoreManagerInterface|null $storeManager */ - public function __construct(\Magento\Framework\UrlInterface $frontendUrlBuilder) - { + public function __construct( + \Magento\Framework\UrlInterface $frontendUrlBuilder, + EncoderInterface $urlEncoder = null, + StoreManagerInterface $storeManager = null + ) { $this->frontendUrlBuilder = $frontendUrlBuilder; + $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance() + ->get(EncoderInterface::class); + $this->storeManager = $storeManager?: ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** @@ -35,12 +61,22 @@ public function getUrl($routePath, $scope, $store) { if ($scope) { $this->frontendUrlBuilder->setScope($scope); - $href = $this->frontendUrlBuilder->getUrl( + $targetUrl = $this->frontendUrlBuilder->getUrl( $routePath, [ '_current' => false, '_nosid' => true, - '_query' => [\Magento\Store\Model\StoreManagerInterface::PARAM_NAME => $store] + '_query' => [ + StoreManagerInterface::PARAM_NAME => $store + ] + ] + ); + $href = $this->frontendUrlBuilder->getUrl( + 'stores/store/switch', + [ + '_current' => false, + '_nosid' => true, + '_query' => $this->prepareRequestQuery($store, $targetUrl) ] ); } else { @@ -55,4 +91,25 @@ public function getUrl($routePath, $scope, $store) return $href; } + + /** + * Prepare request query + * + * @param string $store + * @param string $href + * @return array + */ + private function prepareRequestQuery(string $store, string $href) : array + { + $storeView = $this->storeManager->getDefaultStoreView(); + $query = [ + StoreManagerInterface::PARAM_NAME => $store, + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($href) + ]; + if ($storeView->getCode() !== $store) { + $query['___from_store'] = $storeView->getCode(); + } + + return $query; + } } diff --git a/app/code/Magento/Store/Controller/Store/SwitchAction.php b/app/code/Magento/Store/Controller/Store/SwitchAction.php index de721869c5aba..d8ac1b308d7ed 100644 --- a/app/code/Magento/Store/Controller/Store/SwitchAction.php +++ b/app/code/Magento/Store/Controller/Store/SwitchAction.php @@ -4,6 +4,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Store\Controller\Store; @@ -18,13 +19,15 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreSwitcher; use Magento\Store\Model\StoreSwitcherInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; /** * Handles store switching url and makes redirect. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class SwitchAction extends Action +class SwitchAction extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * @var StoreCookieManagerInterface @@ -89,10 +92,12 @@ public function __construct( public function execute() { $targetStoreCode = $this->_request->getParam( - \Magento\Store\Model\StoreManagerInterface::PARAM_NAME, + \Magento\Store\Model\StoreManagerInterface::PARAM_NAME + ); + $fromStoreCode = $this->_request->getParam( + '___from_store', $this->storeCookieManager->getStoreCodeFromCookie() ); - $fromStoreCode = $this->_request->getParam('___from_store'); $requestedUrlToRedirect = $this->_redirect->getRedirectUrl(); $redirectUrl = $requestedUrlToRedirect; diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index e1bb094e7fc39..a4bca19cfc041 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -70,10 +70,7 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s if ($oldRewrite) { $targetUrl = $targetStore->getBaseUrl(); // look for url rewrite match on the target store - $currentRewrite = $this->urlFinder->findOneByData([ - UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), - UrlRewrite::STORE_ID => $targetStore->getId(), - ]); + $currentRewrite = $this->findCurrentRewrite($oldRewrite, $targetStore); if ($currentRewrite) { $targetUrl .= $currentRewrite->getRequestPath(); } @@ -93,4 +90,26 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s } return $targetUrl; } + + /** + * Look for url rewrite match on the target store + * + * @param UrlRewrite $oldRewrite + * @param StoreInterface $targetStore + * @return UrlRewrite|null + */ + private function findCurrentRewrite(UrlRewrite $oldRewrite, StoreInterface $targetStore) + { + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), + UrlRewrite::STORE_ID => $targetStore->getId(), + ]); + if (!$currentRewrite) { + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::REQUEST_PATH => $oldRewrite->getTargetPath(), + UrlRewrite::STORE_ID => $targetStore->getId(), + ]); + } + return $currentRewrite; + } } From 4926fd55992f45621a2860c9dacd368f8954c1bd Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina Date: Fri, 5 Apr 2019 13:04:59 +0300 Subject: [PATCH 07/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Add check current user permission on category resource --- .../Product/Form/Modifier/CategoriesTest.php | 21 +++++++++++++-- .../Product/Form/Modifier/Categories.php | 26 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index cd6565f32ed18..f9fd16e4e0bf7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; @@ -12,6 +14,7 @@ use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; +use Magento\Framework\AuthorizationInterface; /** * Class CategoriesTest @@ -45,6 +48,11 @@ class CategoriesTest extends AbstractModifierTest */ protected $categoryCollectionMock; + /** + * @var AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + protected function setUp() { parent::setUp(); @@ -63,6 +71,9 @@ protected function setUp() $this->categoryCollectionMock = $this->getMockBuilder(CategoryCollection::class) ->disableOriginalConstructor() ->getMock(); + $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->categoryCollectionFactoryMock->expects($this->any()) ->method('create') @@ -90,6 +101,7 @@ protected function createModel() 'locator' => $this->locatorMock, 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, 'arrayManager' => $this->arrayManagerMock, + 'authorization' => $this->authorizationMock ]); } @@ -130,7 +142,9 @@ public function testModifyMetaLocked($locked) ], ], ]; - + $this->authorizationMock->expects($this->once()) + ->method('isAllowed') + ->willReturn(true); $this->arrayManagerMock->expects($this->any()) ->method('findPath') ->willReturn('path'); @@ -167,7 +181,10 @@ public function testModifyMetaWithCaching() ->with(Categories::CATEGORY_TREE_ID . '_'); $cacheManager->expects($this->once()) ->method('save'); - + $this->authorizationMock->expects($this->once()) + ->method('isAllowed') + ->willReturn(true); + $modifier = $this->createModel(); $cacheContextProperty = new \ReflectionProperty( Categories::class, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 681435851fbde..6af7ba39b9d24 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Model\Locator\LocatorInterface; @@ -14,6 +16,7 @@ use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\AuthorizationInterface; /** * Data provider for categories field of product page @@ -78,6 +81,11 @@ class Categories extends AbstractModifier */ private $serializer; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param LocatorInterface $locator * @param CategoryCollectionFactory $categoryCollectionFactory @@ -85,6 +93,7 @@ class Categories extends AbstractModifier * @param UrlInterface $urlBuilder * @param ArrayManager $arrayManager * @param SerializerInterface $serializer + * @param AuthorizationInterface $authorization */ public function __construct( LocatorInterface $locator, @@ -92,7 +101,8 @@ public function __construct( DbHelper $dbHelper, UrlInterface $urlBuilder, ArrayManager $arrayManager, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + AuthorizationInterface $authorization = null ) { $this->locator = $locator; $this->categoryCollectionFactory = $categoryCollectionFactory; @@ -100,6 +110,7 @@ public function __construct( $this->urlBuilder = $urlBuilder; $this->arrayManager = $arrayManager; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); + $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); } /** @@ -123,12 +134,25 @@ private function getCacheManager() */ public function modifyMeta(array $meta) { + if (!$this->isAllowed()) { + return $meta; + } $meta = $this->createNewCategoryModal($meta); $meta = $this->customizeCategoriesField($meta); return $meta; } + /** + * Check current user permission on category resource + * + * @return bool + */ + private function isAllowed() + { + return $this->authorization->isAllowed('Magento_Catalog::categories'); + } + /** * @inheritdoc * @since 101.0.0 From d9aa6f3d041074826d5fb719855f0a0eb53a6836 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina Date: Tue, 2 Apr 2019 12:13:33 +0300 Subject: [PATCH 08/78] MAGETWO-71835: [Product grid] SC's values aren't sorted alphabetically in the tooltip - Fix static --- app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js index 9d7477eb8ae21..0733efa588991 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js @@ -68,7 +68,7 @@ define([ }); return labels.sort( - function(a, b) { + function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); } ); From 5d04e3c95ce9ade268a1fb0c608f8ea1fe3266ea Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina Date: Mon, 8 Apr 2019 15:35:20 +0300 Subject: [PATCH 09/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Add check current user permission on category resource --- .../Product/Form/Modifier/CategoriesTest.php | 4 +- .../Product/Form/Modifier/Categories.php | 152 +++++++++--------- 2 files changed, 79 insertions(+), 77 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index f9fd16e4e0bf7..7507a748f90ac 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -142,7 +142,7 @@ public function testModifyMetaLocked($locked) ], ], ]; - $this->authorizationMock->expects($this->once()) + $this->authorizationMock->expects($this->exactly(2)) ->method('isAllowed') ->willReturn(true); $this->arrayManagerMock->expects($this->any()) @@ -181,7 +181,7 @@ public function testModifyMetaWithCaching() ->with(Categories::CATEGORY_TREE_ID . '_'); $cacheManager->expects($this->once()) ->method('save'); - $this->authorizationMock->expects($this->once()) + $this->authorizationMock->expects($this->exactly(2)) ->method('isAllowed') ->willReturn(true); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 6af7ba39b9d24..ddfb9d0b8d449 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -134,10 +134,9 @@ private function getCacheManager() */ public function modifyMeta(array $meta) { - if (!$this->isAllowed()) { - return $meta; + if ($this->isAllowed()) { + $meta = $this->createNewCategoryModal($meta); } - $meta = $this->createNewCategoryModal($meta); $meta = $this->customizeCategoriesField($meta); return $meta; @@ -238,88 +237,91 @@ protected function customizeCategoriesField(array $meta) return $meta; } - $meta = $this->arrayManager->merge( - $containerPath, - $meta, - [ - 'arguments' => [ - 'data' => [ - 'config' => [ - 'label' => __('Categories'), - 'dataScope' => '', - 'breakLine' => false, - 'formElement' => 'container', - 'componentType' => 'container', - 'component' => 'Magento_Ui/js/form/components/group', - 'scopeLabel' => __('[GLOBAL]'), - 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), - ], + $value = [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'label' => __('Categories'), + 'dataScope' => '', + 'breakLine' => false, + 'formElement' => 'container', + 'componentType' => 'container', + 'component' => 'Magento_Ui/js/form/components/group', + 'scopeLabel' => __('[GLOBAL]'), + 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ], ], - 'children' => [ - $fieldCode => [ - 'arguments' => [ - 'data' => [ + ], + 'children' => [ + $fieldCode => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'formElement' => 'select', + 'componentType' => 'field', + 'component' => 'Magento_Catalog/js/components/new-category', + 'filterOptions' => true, + 'chipsEnabled' => true, + 'disableLabel' => true, + 'levelsVisibility' => '1', + 'elementTmpl' => 'ui/grid/filters/elements/ui-select', + 'options' => $this->getCategoriesTree(), + 'listens' => [ + 'index=create_category:responseData' => 'setParsed', + 'newOption' => 'toggleOptionSelected' + ], 'config' => [ - 'formElement' => 'select', - 'componentType' => 'field', - 'component' => 'Magento_Catalog/js/components/new-category', - 'filterOptions' => true, - 'chipsEnabled' => true, - 'disableLabel' => true, - 'levelsVisibility' => '1', - 'elementTmpl' => 'ui/grid/filters/elements/ui-select', - 'options' => $this->getCategoriesTree(), - 'listens' => [ - 'index=create_category:responseData' => 'setParsed', - 'newOption' => 'toggleOptionSelected' - ], - 'config' => [ - 'dataScope' => $fieldCode, - 'sortOrder' => 10, - ], + 'dataScope' => $fieldCode, + 'sortOrder' => 10, ], ], ], ], - 'create_category_button' => [ - 'arguments' => [ - 'data' => [ - 'config' => [ - 'title' => __('New Category'), - 'formElement' => 'container', - 'additionalClasses' => 'admin__field-small', - 'componentType' => 'container', - 'component' => 'Magento_Ui/js/form/components/button', - 'template' => 'ui/form/components/button/container', - 'actions' => [ - [ - 'targetName' => 'product_form.product_form.create_category_modal', - 'actionName' => 'toggleModal', - ], - [ - 'targetName' => - 'product_form.product_form.create_category_modal.create_category', - 'actionName' => 'render' - ], - [ - 'targetName' => - 'product_form.product_form.create_category_modal.create_category', - 'actionName' => 'resetForm' - ] - ], - 'additionalForGroup' => true, - 'provider' => false, - 'source' => 'product_details', - 'displayArea' => 'insideGroup', - 'sortOrder' => 20, - 'dataScope' => $fieldCode, + ], + ] + ]; + if ($this->isAllowed()) { + $value['children']['create_category_button'] = [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'title' => __('New Category'), + 'formElement' => 'container', + 'additionalClasses' => 'admin__field-small', + 'componentType' => 'container', + 'component' => 'Magento_Ui/js/form/components/button', + 'template' => 'ui/form/components/button/container', + 'actions' => [ + [ + 'targetName' => 'product_form.product_form.create_category_modal', + 'actionName' => 'toggleModal', ], + [ + 'targetName' => + 'product_form.product_form.create_category_modal.create_category', + 'actionName' => 'render' + ], + [ + 'targetName' => + 'product_form.product_form.create_category_modal.create_category', + 'actionName' => 'resetForm' + ] ], - ] - ] + 'additionalForGroup' => true, + 'provider' => false, + 'source' => 'product_details', + 'displayArea' => 'insideGroup', + 'sortOrder' => 20, + 'dataScope' => $fieldCode, + ], + ], ] - ] + ]; + } + $meta = $this->arrayManager->merge( + $containerPath, + $meta, + $value ); return $meta; From fad6db4b218c62852e1e96f44db59e34f5e02e91 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina Date: Wed, 10 Apr 2019 16:57:24 +0300 Subject: [PATCH 10/78] MAGETWO-71835: [Product grid] SC's values aren't sorted alphabetically in the tooltip - Add js unit test --- .../Ui/base/js/grid/columns/expandable.test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js index 1c3b51919f5f3..ffc46af01f9e4 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js @@ -69,5 +69,22 @@ define([ expect(expandable.getLabel).toHaveBeenCalled(); }); }); + + describe('getLabelsArray method', function () { + it('check if label array sort alphabetically case insensitive', function () { + record['shared_catalog'].push(1, 2 , 3); + expandable.options.push({ + label: 'Default', + value: '1' + }, { + label: 'Label', + value: '2' + }, { + label: 'default', + value: '3' + }); + expect(expandable.getLabelsArray(record)).toEqual(['Default', 'default', 'Label']); + }); + }); }); }); From e5a2cdb5ee2d09785b0418d3e7acd019e085ff62 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina Date: Wed, 10 Apr 2019 17:14:58 +0300 Subject: [PATCH 11/78] MAGETWO-71835: [Product grid] SC's values aren't sorted alphabetically in the tooltip - Sort values case insensitive --- .../Magento/Ui/view/base/web/js/grid/columns/expandable.js | 4 ++-- .../code/Magento/Ui/base/js/grid/columns/expandable.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js index 0733efa588991..b694f24031271 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/expandable.js @@ -68,8 +68,8 @@ define([ }); return labels.sort( - function (a, b) { - return a.toLowerCase().localeCompare(b.toLowerCase()); + function (labelFirst, labelSecond) { + return labelFirst.toLowerCase().localeCompare(labelSecond.toLowerCase()); } ); }, diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js index ffc46af01f9e4..b8b6dce52c05d 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/expandable.test.js @@ -72,7 +72,7 @@ define([ describe('getLabelsArray method', function () { it('check if label array sort alphabetically case insensitive', function () { - record['shared_catalog'].push(1, 2 , 3); + record['shared_catalog'].push(1, 2, 3); expandable.options.push({ label: 'Default', value: '1' From 575d7d97b13b44a90e129e29f7234190efbef988 Mon Sep 17 00:00:00 2001 From: vprohorov Date: Thu, 11 Apr 2019 13:29:26 +0300 Subject: [PATCH 12/78] MAGETWO-63599: [GitHub] catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes #8204 - Fix for integration tests --- lib/internal/Magento/Framework/Image/Adapter/Gd2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index 300f7a75ab509..d423fcfc6773a 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -60,8 +60,8 @@ protected function _reset() */ public function open($filename) { - if (filesize($filename) === 0) { - throw new \InvalidArgumentException("Wrong file size: '{$filename}'."); + if (!$filename || filesize($filename) === 0) { + throw new \InvalidArgumentException('Wrong file'); } $this->_fileName = $filename; $this->_reset(); From 4b8dcbc0131ce983d28c08d364346781b7b0d532 Mon Sep 17 00:00:00 2001 From: Lusine Papyan Date: Mon, 15 Apr 2019 11:34:51 +0400 Subject: [PATCH 13/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Added automated test script --- .../Mftf/Section/AdminProductFormSection.xml | 1 + ...ctedUserAddCategoryFromProductPageTest.xml | 89 +++++++++++++++++++ .../AdminCreateRoleActionGroup.xml | 19 ++++ 3 files changed, 109 insertions(+) create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index da282d06145aa..ec90b4eda8d06 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -70,6 +70,7 @@ +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml new file mode 100644 index 0000000000000..be18a9980bf76 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -0,0 +1,89 @@ + + + + + + + + + <description value="Adding new category from product page by restricted user"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-99063"/> + <useCaseId value="MAGETWO-69893"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <comment userInput="Create category" stepKey="commentCreateCategory"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + </before> + <after> + <!--Delete created product--> + <comment userInput="Delete created product" stepKey="commentDeleteProduct"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetFiltersIfExist"/> + <actionGroup ref="SignOut" stepKey="signOut"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Delete created data--> + <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> + <actionGroup ref="AdminDeleteCreatedRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="newAdmin"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Create user role--> + <comment userInput="Create user role" stepKey="commentCreateUserRole"/> + <actionGroup ref="AdminFillUserRoleRequiredData" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Stores"/> + </actionGroup> + <click selector="{{AdminEditRoleInfoSection.roleResourcesTab}}" stepKey="clickRoleResourcesTab" /> + <actionGroup ref="AdminAddRestrictedRole" stepKey="addRestrictedRoleStores"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Stores"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRole" stepKey="addRestrictedRoleProducts"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Products"/> + </actionGroup> + <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> + <see userInput="You saved the role." stepKey="seeUserRoleSavedMessage"/> + <!--Create user and assign role to it--> + <comment userInput="Create user and assign role to it" stepKey="commentCreateUser"/> + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="newAdmin"/> + </actionGroup> + <!--Log out of admin and login with newly created user--> + <comment userInput="Log out of admin and login with newly created user" stepKey="commentLoginWithNewUser"/> + <actionGroup ref="SignOut" stepKey="signOut"/> + <actionGroup ref="LoginAsAnyUser" stepKey="loginActionGroup"> + <argument name="uname" value="{{newAdmin.username}}"/> + <argument name="passwd" value="{{newAdmin.password}}"/> + </actionGroup> + <!--Go to create product page--> + <comment userInput="Go to create product page" stepKey="commentGoCreateProductPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"/> + <dontSeeElement selector="{{AdminProductFormSection.newCategoryButton}}" stepKey="dontSeeNewCategoryButton"/> + <!--Fill product data and assign to category--> + <comment userInput="Fill product data and assign to category" stepKey="commentFillProductData"/> + <actionGroup ref="fillMainProductForm" stepKey="fillMainProductForm"/> + <actionGroup ref="SetCategoryByName" stepKey="addCategoryToProduct"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml index da08ac469b7c4..ed45085cd9cc1 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml @@ -24,6 +24,25 @@ <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> <waitForPageLoad stepKey="waitForPageLoad2" /> </actionGroup> + <actionGroup name="AdminFillUserRoleRequiredData" extends="AdminCreateRoleActionGroup"> + <remove keyForRemoval="clickRoleResourcesTab"/> + <remove keyForRemoval="waitForScopeSelection"/> + <remove keyForRemoval="selectResourceAccessCustom"/> + <remove keyForRemoval="waitForElementVisible"/> + <remove keyForRemoval="clickContentBlockCheckbox"/> + <remove keyForRemoval="clickSaveRoleButton"/> + <remove keyForRemoval="waitForPageLoad2"/> + </actionGroup> + <actionGroup name="AdminAddRestrictedRole" extends="AdminCreateRoleActionGroup"> + <remove keyForRemoval="navigateToNewRole"/> + <remove keyForRemoval="waitForPageLoad1"/> + <remove keyForRemoval="fillRoleName"/> + <remove keyForRemoval="enterPassword"/> + <remove keyForRemoval="clickRoleResourcesTab"/> + <remove keyForRemoval="clickSaveRoleButton"/> + <remove keyForRemoval="waitForPageLoad2"/> + <scrollTo selector="{{AdminEditRoleInfoSection.blockName('restrictedRole')}}" x="0" y="-100" stepKey="scrollToResourceElement" after="selectResourceAccessCustom"/> + </actionGroup> <!--Create new role--> <actionGroup name="AdminCreateRole"> <arguments> From 2f6dca52fdd44d8c272bb9f261e7c53bcb408c26 Mon Sep 17 00:00:00 2001 From: Davit_Zakharyan <davit_zakharyan@epam.com> Date: Tue, 16 Apr 2019 14:21:59 +0400 Subject: [PATCH 14/78] MAGETWO-93061: CMS page of second website with same URLkey as first website, show content of First website instead of second website content. - Added automated test script. --- .../CreateNewPageWithAllValuesActionGroup.xml | 6 ++++++ app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml index c51e673139af9..a459c41ccb41b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml @@ -28,4 +28,10 @@ <click selector="{{CmsNewPageHierarchySection.header}}" stepKey="clickHierarchy"/> <click selector="{{CmsNewPageHierarchySection.selectHierarchy(selectHierarchyOpt)}}" stepKey="clickPageCheckBoxes"/> </actionGroup> + <actionGroup name="CreateNewPageWithAllValuesAndContent" extends="CreateNewPageWithAllValues"> + <arguments> + <argument name="pageContent" type="string"/> + </arguments> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{pageContent}}" stepKey="fillContentField" after="fillFieldContentHeading"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml index 2ec2eccba2344..ef13d7634a877 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml @@ -20,6 +20,15 @@ <data key="content">Sample page content. Yada yada yada.</data> <data key="identifier" unique="suffix">test-page-</data> </entity> + <entity name="customCmsPage" extends="_defaultCmsPage" type="cms_page"> + <data key="content">Test content data1</data> + <data key="identifier">url_key</data> + </entity> + <entity name="customCmsPage2" extends="_defaultCmsPage" type="cms_page"> + <data key="title">Test Second CMS Page</data> + <data key="content">Test content data2</data> + <data key="identifier">url_key</data> + </entity> <entity name="_duplicatedCMSPage" type="cms_page"> <data key="title">testpage</data> <data key="content_heading">Test Content Heading</data> From 8e16a67b54095dec2426243340c87bcb055c9cca Mon Sep 17 00:00:00 2001 From: Lusine Papyan <Lusine_Papyan@epam.com> Date: Wed, 17 Apr 2019 10:14:39 +0400 Subject: [PATCH 15/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Updated automated test script --- .../Mftf/ActionGroup/AdminUserActionGroup.xml | 10 ++++----- ...ctedUserAddCategoryFromProductPageTest.xml | 21 +++++++++++-------- .../ActionGroup/SwitchAccountActionGroup.xml | 7 +++++-- .../AdminCreateRoleActionGroup.xml | 1 + .../AdminCreateUserActionGroup.xml | 12 +++++------ .../Section/AdminEditRoleResourcesSection.xml | 17 +++++++++++++++ .../Mftf/Section/AdminUserGridSection.xml | 2 +- 7 files changed, 47 insertions(+), 23 deletions(-) create mode 100644 app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml index 3f8bdaa4cd6bd..841dfb5607cd1 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml @@ -38,12 +38,13 @@ <waitForPageLoad stepKey="waitForSaveUser" time="10"/> <see userInput="You saved the user." stepKey="seeSuccessMessage" /> </actionGroup> - <!--Delete User--> <actionGroup name="AdminDeleteNewUserActionGroup"> - - <click stepKey="clickOnUser" selector="{{AdminDeleteUserSection.theUser}}"/> - <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <arguments> + <argument name="userName" type="string" defaultValue="John"/> + </arguments> + <click stepKey="clickOnUser" selector="{{AdminDeleteUserSection.theUser(userName)}}"/> + <fillField stepKey="typeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> <waitForPageLoad stepKey="waitForDeletePopupOpen" time="5"/> @@ -51,5 +52,4 @@ <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see userInput="You deleted the user." stepKey="seeSuccessMessage" /> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index be18a9980bf76..55a7a7632dea6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -31,15 +31,19 @@ <argument name="sku" value="{{_defaultProduct.sku}}"/> </actionGroup> <actionGroup ref="resetProductGridToDefaultView" stepKey="resetFiltersIfExist"/> - <actionGroup ref="SignOut" stepKey="signOut"/> + <actionGroup ref="logout" stepKey="logoutOfUser"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <!--Delete created data--> <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> - <actionGroup ref="AdminDeleteCreatedRoleActionGroup" stepKey="deleteUserRole"> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> <argument name="role" value="adminRole"/> </actionGroup> - <actionGroup ref="AdminDeleteCreatedUserActionGroup" stepKey="deleteUser"> - <argument name="user" value="newAdmin"/> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> </actionGroup> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="logout" stepKey="logoutOfAdmin"/> @@ -65,14 +69,13 @@ <comment userInput="Create user and assign role to it" stepKey="commentCreateUser"/> <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> <argument name="role" value="adminRole"/> - <argument name="User" value="newAdmin"/> + <argument name="User" value="admin2"/> </actionGroup> <!--Log out of admin and login with newly created user--> <comment userInput="Log out of admin and login with newly created user" stepKey="commentLoginWithNewUser"/> - <actionGroup ref="SignOut" stepKey="signOut"/> - <actionGroup ref="LoginAsAnyUser" stepKey="loginActionGroup"> - <argument name="uname" value="{{newAdmin.username}}"/> - <argument name="passwd" value="{{newAdmin.password}}"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + <actionGroup ref="LoginNewUser" stepKey="loginActionGroup"> + <argument name="user" value="admin2"/> </actionGroup> <!--Go to create product page--> <comment userInput="Go to create product page" stepKey="commentGoCreateProductPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml index 4c59edbcb8057..40bf68db6ea68 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml @@ -19,9 +19,12 @@ <!--Login New User--> <actionGroup name="LoginNewUser"> + <arguments> + <argument name="user" defaultValue="NewAdmin"/> + </arguments> <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}" stepKey="navigateToAdmin"/> - <fillField userInput="{{NewAdmin.username}}" selector="{{LoginFormSection.username}}" stepKey="fillUsername"/> - <fillField userInput="{{NewAdmin.password}}" selector="{{LoginFormSection.password}}" stepKey="fillPassword"/> + <fillField userInput="{{user.username}}" selector="{{LoginFormSection.username}}" stepKey="fillUsername"/> + <fillField userInput="{{user.password}}" selector="{{LoginFormSection.password}}" stepKey="fillPassword"/> <click selector="{{LoginFormSection.signIn}}" stepKey="clickLogin"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml index ed45085cd9cc1..22ce7a54a6355 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml @@ -39,6 +39,7 @@ <remove keyForRemoval="fillRoleName"/> <remove keyForRemoval="enterPassword"/> <remove keyForRemoval="clickRoleResourcesTab"/> + <remove keyForRemoval="waitForScopeSelection"/> <remove keyForRemoval="clickSaveRoleButton"/> <remove keyForRemoval="waitForPageLoad2"/> <scrollTo selector="{{AdminEditRoleInfoSection.blockName('restrictedRole')}}" x="0" y="-100" stepKey="scrollToResourceElement" after="selectResourceAccessCustom"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml index 303713132d2b0..3f1aa73361b38 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml @@ -15,12 +15,12 @@ <amOnPage url="{{AdminUsersPage.url}}" stepKey="amOnAdminUsersPage"/> <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> <click selector="{{AdminCreateUserSection.create}}" stepKey="clickToCreateNewUser"/> - <fillField selector="{{AdminEditUserSection.usernameTextField}}" userInput="{{newAdmin.username}}" stepKey="enterUserName" /> - <fillField selector="{{AdminEditUserSection.firstNameTextField}}" userInput="{{newAdmin.firstName}}" stepKey="enterFirstName" /> - <fillField selector="{{AdminEditUserSection.lastNameTextField}}" userInput="{{newAdmin.lastName}}" stepKey="enterLastName" /> - <fillField selector="{{AdminEditUserSection.emailTextField}}" userInput="{{newAdmin.username}}@magento.com" stepKey="enterEmail" /> - <fillField selector="{{AdminEditUserSection.passwordTextField}}" userInput="{{newAdmin.password}}" stepKey="enterPassword" /> - <fillField selector="{{AdminEditUserSection.pwConfirmationTextField}}" userInput="{{newAdmin.password}}" stepKey="confirmPassword" /> + <fillField selector="{{AdminEditUserSection.usernameTextField}}" userInput="{{User.username}}" stepKey="enterUserName" /> + <fillField selector="{{AdminEditUserSection.firstNameTextField}}" userInput="{{User.firstName}}" stepKey="enterFirstName" /> + <fillField selector="{{AdminEditUserSection.lastNameTextField}}" userInput="{{User.lastName}}" stepKey="enterLastName" /> + <fillField selector="{{AdminEditUserSection.emailTextField}}" userInput="{{User.username}}@magento.com" stepKey="enterEmail" /> + <fillField selector="{{AdminEditUserSection.passwordTextField}}" userInput="{{User.password}}" stepKey="enterPassword" /> + <fillField selector="{{AdminEditUserSection.pwConfirmationTextField}}" userInput="{{User.password}}" stepKey="confirmPassword" /> <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterCurrentPassword" /> <scrollToTopOfPage stepKey="scrollToTopOfPage" /> <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRole" /> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml new file mode 100644 index 0000000000000..6bcfe48addb43 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.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="AdminEditRoleResourcesSection"> + <element name="roleScopes" type="select" selector="#gws_is_all"/> + <element name="resourceAccess" type="select" selector="#all"/> + <element name="resources" type="checkbox" selector="#role_info_tabs_account"/> + <element name="storeName" type="checkbox" selector="//label[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="reportsCheckbox" type="text" selector="//li[@data-id='Magento_Reports::report']//a[text()='Reports']"/> + <element name="userRoles" type="text" selector="//span[contains(text(), 'User Roles')]"/> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml index c21a8b875e95b..32e834615a270 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml @@ -16,7 +16,7 @@ </section> <section name="AdminDeleteUserSection"> - <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> + <element name="theUser" selector="//td[contains(text(), '{{userName}}')]" type="button" parameterized="true"/> <element name="password" selector="#user_current_password" type="input"/> <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> From 4e1139285d15a1bc63c48325bb3becaa555014e3 Mon Sep 17 00:00:00 2001 From: vprohorov <prohorov.vital@gmail.com> Date: Thu, 18 Apr 2019 17:13:32 +0300 Subject: [PATCH 16/78] MAGETWO-88905: Import Customer ("gender" field) issue - Fixed customer import gender field issue --- .../Magento/CustomerImportExport/Model/Import/Customer.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index ab940c9e84533..83c68062bcbc1 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -423,6 +423,9 @@ protected function _prepareDataForUpdate(array $rowData) $attributeParameters = $this->_attributes[$attributeCode]; if (in_array($attributeParameters['type'], ['select', 'boolean'])) { $value = $this->getSelectAttrIdByValue($attributeParameters, $value); + if ($attributeCode === CustomerInterface::GENDER && $value === 0) { + $value = null; + } } elseif ('multiselect' == $attributeParameters['type']) { $ids = []; foreach (explode($multiSeparator, mb_strtolower($value)) as $subValue) { From d739a3fd0c6ac30f5923cd13de60410b06059333 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Fri, 3 May 2019 16:19:07 +0300 Subject: [PATCH 17/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../Product/Attribute/CreateOptions.php | 142 ++++++++++++++---- .../js/variations/steps/attributes_values.js | 40 ++++- 2 files changed, 148 insertions(+), 34 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php index cfa2562d974f4..841216a067808 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php @@ -4,12 +4,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Json\Helper\Data; +/** + * Creates options for product attributes + */ class CreateOptions extends Action implements HttpPostActionInterface { /** @@ -20,28 +28,33 @@ class CreateOptions extends Action implements HttpPostActionInterface const ADMIN_RESOURCE = 'Magento_Catalog::products'; /** - * @var \Magento\Framework\Json\Helper\Data + * @var Data */ protected $jsonHelper; /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory + * @var AttributeFactory */ protected $attributeFactory; + /** + * @var ProductAttributeInterface[] + */ + private $attributes; + /** * @param Action\Context $context - * @param \Magento\Framework\Json\Helper\Data $jsonHelper + * @param Data $jsonHelper * @param AttributeFactory $attributeFactory */ public function __construct( Action\Context $context, - \Magento\Framework\Json\Helper\Data $jsonHelper, + Data $jsonHelper, AttributeFactory $attributeFactory ) { + parent::__construct($context); $this->jsonHelper = $jsonHelper; $this->attributeFactory = $attributeFactory; - parent::__construct($context); } /** @@ -51,7 +64,15 @@ public function __construct( */ public function execute() { - $this->getResponse()->representJson($this->jsonHelper->jsonEncode($this->saveAttributeOptions())); + try { + $output = $this->saveAttributeOptions(); + } catch (LocalizedException $e) { + $output = [ + 'error' => true, + 'message' => $e->getMessage(), + ]; + } + $this->getResponse()->representJson($this->jsonHelper->jsonEncode($output)); } /** @@ -61,31 +82,100 @@ public function execute() * @TODO Move this logic to configurable product type model * when full set of operations for attribute options during * product creation will be implemented: edit labels, remove, reorder. - * Currently only addition of options to end and removal of just added option is supported. + * Currently only addition of options is supported. + * @throws LocalizedException */ protected function saveAttributeOptions() { - $options = (array)$this->getRequest()->getParam('options'); + $attributeIds = $this->getUpdatedAttributeIds(); $savedOptions = []; - foreach ($options as $option) { - if (isset($option['label']) && isset($option['is_new'])) { - $attribute = $this->attributeFactory->create(); - $attribute->load($option['attribute_id']); - $optionsBefore = $attribute->getSource()->getAllOptions(false); - $attribute->setOption( - [ - 'value' => ['option_0' => [$option['label']]], - 'order' => ['option_0' => count($optionsBefore) + 1], - ] - ); - $attribute->save(); - $attribute = $this->attributeFactory->create(); - $attribute->load($option['attribute_id']); - $optionsAfter = $attribute->getSource()->getAllOptions(false); - $newOption = array_pop($optionsAfter); - $savedOptions[$option['id']] = $newOption['value']; + foreach ($attributeIds as $attributeId => $newOptions) { + $attribute = $this->getAttribute($attributeId); + $this->checkUnique($attribute, $newOptions); + foreach ($newOptions as $newOption) { + $lastAddedOption = $this->saveOption($attribute, $newOption); + $savedOptions[$newOption['id']] = $lastAddedOption['value']; } } + return $savedOptions; } + + /** + * Checks unique values + * + * @param ProductAttributeInterface $attribute + * @param array $newOptions + * @return void + * @throws LocalizedException + */ + private function checkUnique(ProductAttributeInterface $attribute, array $newOptions) + { + $originalOptions = $attribute->getSource()->getAllOptions(false); + $allOptions = array_merge($originalOptions, $newOptions); + $optionValues = array_map(function ($option) { + return $option['label']; + }, $allOptions); + + $uniqueValues = array_unique($optionValues); + $duplicates = array_diff_assoc($optionValues, $uniqueValues); + if ($duplicates) { + throw new LocalizedException(__('Attributes must have unique option values')); + } + } + + /** + * Loads the product attribute by the id + * + * @param int $attributeId + * @return ProductAttributeInterface + */ + private function getAttribute(int $attributeId) + { + if (!isset($this->attributes[$attributeId])) { + $attribute = $this->attributeFactory->create(); + $this->attributes[$attributeId] = $attribute->load($attributeId); + } + + return $this->attributes[$attributeId]; + } + + /** + * Retrieve updated attribute ids with new options + * + * @return array + */ + private function getUpdatedAttributeIds() + { + $options = (array)$this->getRequest()->getParam('options'); + $updatedAttributeIds = []; + foreach ($options as $option) { + if (isset($option['label'], $option['is_new'], $option['attribute_id'])) { + $updatedAttributeIds[$option['attribute_id']][] = $option; + } + } + + return $updatedAttributeIds; + } + + /** + * Saves the option + * + * @param ProductAttributeInterface $attribute + * @param array $newOption + * @return array + */ + private function saveOption(ProductAttributeInterface $attribute, array $newOption) + { + $optionsBefore = $attribute->getSource()->getAllOptions(false); + $attribute->setOption( + [ + 'value' => ['option_0' => [$newOption['label']]], + 'order' => ['option_0' => count($optionsBefore) + 1], + ] + ); + $attribute->save(); + $optionsAfter = $attribute->getSource()->getAllOptions(false); + return array_pop($optionsAfter); + } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index 1fa65e03f54e0..7331601c538f0 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -184,32 +184,56 @@ define([ * @return {Boolean} */ saveOptions: function () { - var options = []; + var newOptions = [], + errorsInAttributes = [], + allOptions = []; this.attributes.each(function (attribute) { - attribute.chosenOptions.each(function (id) { + + attribute.options.each(function (element) { var option = attribute.options.findWhere({ - id: id, - 'is_new': true + id: element.id }); - if (option) { - options.push(option); + if (option.is_new === true) { + newOptions.push(option); + } + + if (typeof allOptions[option.attribute_id] === 'undefined') { + allOptions[option.attribute_id] = []; } + + if (typeof allOptions[option.attribute_id][option.label] !== 'undefined') { + errorsInAttributes.push(attribute); + } + + allOptions[option.attribute_id][option.label] = option.label; }); }); - if (!options.length) { + if (errorsInAttributes.length) { + var errorMessage = $.mage.__('Attributes must have unique option values'); + throw new Error(errorMessage); + } + + if (!newOptions.length) { return false; } + $.ajax({ type: 'POST', url: this.createOptionsUrl, data: { - options: options + options: newOptions }, showLoader: true }).done(function (savedOptions) { + if (savedOptions.error) { + this.notificationMessage.error = savedOptions.error; + this.notificationMessage.text = savedOptions.message; + return; + } + this.attributes.each(function (attribute) { _.each(savedOptions, function (newOptionId, oldOptionId) { var option = attribute.options.findWhere({ From 1a62a1094829fc4d00b68466d365aaf1d32deadf Mon Sep 17 00:00:00 2001 From: Kunal Soni <kunal@ranosys.com> Date: Sat, 4 May 2019 14:10:08 +0530 Subject: [PATCH 18/78] Resolve issue 20038 --- app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js index ab01565d7f1e5..8d22199aa85ca 100644 --- a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js +++ b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js @@ -300,10 +300,10 @@ define([ submitOrder: function () { this.$selector.validate().form(); this.$selector.trigger('afterValidate.beforeSubmit'); - $('body').trigger('processStop'); - + // validate parent form if (this.$selector.validate().errorList.length) { + $('body').trigger('processStop'); return false; } From 0e70751841cc4cdb1141f548053654405e3f3fcf Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Tue, 14 May 2019 11:54:35 +0300 Subject: [PATCH 19/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../js/variations/steps/attributes_values.js | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index 7331601c538f0..0287d10b247ed 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -185,35 +185,31 @@ define([ */ saveOptions: function () { var newOptions = [], - errorsInAttributes = [], - allOptions = []; + allOptions = [], + attributesWithDuplicateOptions = []; this.attributes.each(function (attribute) { + allOptions[attribute.id] = []; attribute.options.each(function (element) { var option = attribute.options.findWhere({ id: element.id }); - if (option.is_new === true) { + if (option['is_new'] === true) { newOptions.push(option); } - if (typeof allOptions[option.attribute_id] === 'undefined') { - allOptions[option.attribute_id] = []; + if (typeof allOptions[attribute.id][option.label] !== 'undefined') { + attributesWithDuplicateOptions.push(attribute); } - if (typeof allOptions[option.attribute_id][option.label] !== 'undefined') { - errorsInAttributes.push(attribute); - } - - allOptions[option.attribute_id][option.label] = option.label; + allOptions[attribute.id][option.label] = option.label; }); }); - if (errorsInAttributes.length) { - var errorMessage = $.mage.__('Attributes must have unique option values'); - throw new Error(errorMessage); + if (attributesWithDuplicateOptions.length) { + throw new Error($.mage.__('Attributes must have unique option values')); } if (!newOptions.length) { @@ -231,6 +227,7 @@ define([ if (savedOptions.error) { this.notificationMessage.error = savedOptions.error; this.notificationMessage.text = savedOptions.message; + return; } From 48e16778fe34aabdd282e7042f89ccaaf323e775 Mon Sep 17 00:00:00 2001 From: vprohorov <prohorov.vital@gmail.com> Date: Wed, 15 May 2019 13:32:03 +0300 Subject: [PATCH 20/78] MAGETWO-88905: Import Customer ("gender" field) issue - Fixed static test --- .../Model/Import/Customer.php | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 83c68062bcbc1..01b7901916101 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -288,9 +288,12 @@ private function getCustomerEntityFieldsToUpdate(array $entitiesToUpdate): array { $firstCustomer = reset($entitiesToUpdate); $columnsToUpdate = array_keys($firstCustomer); - $customerFieldsToUpdate = array_filter($this->customerFields, function ($field) use ($columnsToUpdate) { - return in_array($field, $columnsToUpdate); - }); + $customerFieldsToUpdate = array_filter( + $this->customerFields, + function ($field) use ($columnsToUpdate) { + return in_array($field, $columnsToUpdate); + } + ); return $customerFieldsToUpdate; } @@ -523,9 +526,9 @@ protected function _importData() $attributesToSave[$tableName] = []; } $attributesToSave[$tableName] = array_diff_key( - $attributesToSave[$tableName], - $customerAttributes - ) + $customerAttributes; + $attributesToSave[$tableName], + $customerAttributes + ) + $customerAttributes; } } } @@ -582,12 +585,12 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) } // check password if (isset( - $rowData['password'] - ) && strlen( - $rowData['password'] - ) && $this->string->strlen( - $rowData['password'] - ) < self::MIN_PASSWORD_LENGTH + $rowData['password'] + ) && strlen( + $rowData['password'] + ) && $this->string->strlen( + $rowData['password'] + ) < self::MIN_PASSWORD_LENGTH ) { $this->addRowError(self::ERROR_PASSWORD_LENGTH, $rowNumber); } From 97d15a47932f506e379b9af763649f970fcf713e Mon Sep 17 00:00:00 2001 From: Lilit Sargsyan <Lilit_Sargsyan@epam.com> Date: Mon, 20 May 2019 16:20:26 +0400 Subject: [PATCH 21/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values - Added automated test script. --- .../AdminConfigurableProductActionGroup.xml | 21 +++- ...bleProductAttributeValueUniquenessTest.xml | 97 +++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 5a172ca5eabdf..8b409935eb5c8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -205,7 +205,26 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnThirdNextButton"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFourthNextButton"/> </actionGroup> - + <actionGroup name="adminSelectAttributeForConfigurableProductFromPage"> + <arguments> + <argument name="productAttributeCode" type="string" defaultValue="{{dropdownProductAttribute.attribute_code}}"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickToFilterAttributeNames"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="waitForAttributeCodeInputBeVisible"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{productAttributeCode}}" stepKey="fillAttributeName"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="applyFilter"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="selectFilteredAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + </actionGroup> + <actionGroup name="createAnOptionForAttribute"> + <arguments> + <argument name="optionName" type="string" defaultValue="opt1"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateFirstNewValue"/> + <fillField userInput="{{optionName}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + </actionGroup> <actionGroup name="changeProductConfigurationsInGrid"> <arguments> <argument name="firstOption" type="entity"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml new file mode 100644 index 0000000000000..3c61ff74c5633 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -0,0 +1,97 @@ +<?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="AdminCheckConfigurableProductAttributeValueUniquenessTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Edit a configurable product in admin"/> + <title value="Check attribute value uniqueness in configurable products"/> + <description value="Check attribute value uniqueness in configurable products"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-99519"/> + <useCaseId value="MAGETWO-99443"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="dropdownProductAttribute" stepKey="createProductAttribute"/> + </before> + <after> + <!--Delete created data--> + <comment userInput="Delete created data" stepKey="deleteCreatedData"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProductAndOptions"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + <!--Create configurable product--> + <comment userInput="Create configurable product" stepKey="createConfProd"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Go to created product page--> + <comment userInput="Go to created product page" stepKey="goToProdPage"/> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductGrid"/> + <waitForPageLoad stepKey="waitForProductPage1"/> + <actionGroup ref="filterProductGridByName2" stepKey="filterByName"> + <argument name="name" value="$$createConfigProduct.name$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductName"/> + <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <!--Create configurations for the product--> + <comment userInput="Create configurations for the product" stepKey="createConfigurations"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations1"/> + <waitForPageLoad stepKey="waitForSelectAttributesPage1"/> + <actionGroup ref="AdminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute1"> + <argument name="attributeName" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="createAnOptionForAttribute" stepKey="createOption1ForSelectedAttribute"> + <argument name="optionName" value="opt1"/> + </actionGroup> + <actionGroup ref="createAnOptionForAttribute" stepKey="createOption2ForSelectedAttribute"> + <argument name="optionName" value="opt2"/> + </actionGroup> + <!--Proceed generation--> + <comment userInput="Proceed generation" stepKey="proceedGeneration"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext1"/> + <waitForPageLoad stepKey="waitForStep3Page"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext2"/> + <waitForPageLoad stepKey="waitForStep4Page"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickGenerateProducts"/> + <waitForPageLoad stepKey="waitProductGeneration"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForPageLoad stepKey="waitForProductSave"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <waitForPageLoad stepKey="waitForProductPage2"/> + <seeInCurrentUrl url="{{ProductCatalogPage.url}}" stepKey="seeInProductUrl"/> + <!--Add an option with existing name--> + <comment userInput="Add an option with existing name" stepKey="addAnOptionWithExistingName"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab2"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations2"/> + <waitForPageLoad stepKey="waitForSelectAttributesPage2"/> + <actionGroup ref="AdminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute2"> + <argument name="attributeName" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="createAnOptionForAttribute" stepKey="createAnOptionWithExistingName"> + <argument name="optionName" value="opt1"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextToSubmitOption"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--Assert Error message--> + <comment userInput="Assert Error message" stepKey="assertErrorMsg"/> + <see userInput="Attributes must have unique option values" stepKey="verifyErrorMessage"/> + </test> +</tests> From c55a86d951968d6e8d591e9b2b34c9764f4a2dfc Mon Sep 17 00:00:00 2001 From: Lilit Sargsyan <Lilit_Sargsyan@epam.com> Date: Wed, 22 May 2019 12:57:34 +0400 Subject: [PATCH 22/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values - Updated automated test script. --- ...inCheckConfigurableProductAttributeValueUniquenessTest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index 3c61ff74c5633..61cce181efe61 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -55,7 +55,7 @@ <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations1"/> <waitForPageLoad stepKey="waitForSelectAttributesPage1"/> - <actionGroup ref="AdminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute1"> + <actionGroup ref="adminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute1"> <argument name="attributeName" value="$$createProductAttribute.attribute_code$$"/> </actionGroup> <actionGroup ref="createAnOptionForAttribute" stepKey="createOption1ForSelectedAttribute"> @@ -82,7 +82,7 @@ <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab2"/> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations2"/> <waitForPageLoad stepKey="waitForSelectAttributesPage2"/> - <actionGroup ref="AdminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute2"> + <actionGroup ref="adminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute2"> <argument name="attributeName" value="$$createProductAttribute.attribute_code$$"/> </actionGroup> <actionGroup ref="createAnOptionForAttribute" stepKey="createAnOptionWithExistingName"> From f201c3207d1bca9c1552ef2be837f48249a4f8b4 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Thu, 23 May 2019 17:06:26 +0300 Subject: [PATCH 23/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../Adminhtml/Product/Attribute/CreateOptions.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php index 841216a067808..2788a4947aa46 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php @@ -113,9 +113,12 @@ private function checkUnique(ProductAttributeInterface $attribute, array $newOpt { $originalOptions = $attribute->getSource()->getAllOptions(false); $allOptions = array_merge($originalOptions, $newOptions); - $optionValues = array_map(function ($option) { - return $option['label']; - }, $allOptions); + $optionValues = array_map( + function ($option) { + return $option['label']; + }, + $allOptions + ); $uniqueValues = array_unique($optionValues); $duplicates = array_diff_assoc($optionValues, $uniqueValues); From c2297c78627d728ae96188380c5b7bfd19dfd1b3 Mon Sep 17 00:00:00 2001 From: vprohorov <prohorov.vital@gmail.com> Date: Tue, 28 May 2019 02:51:00 +0300 Subject: [PATCH 24/78] MAGETWO-88905: Import Customer ("gender" field) issue - Static tests fixes --- .../Model/Import/Customer.php | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 01b7901916101..14759bd130f2b 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -525,10 +525,8 @@ protected function _importData() if (!isset($attributesToSave[$tableName])) { $attributesToSave[$tableName] = []; } - $attributesToSave[$tableName] = array_diff_key( - $attributesToSave[$tableName], - $customerAttributes - ) + $customerAttributes; + $attributes = array_diff_key($attributesToSave[$tableName], $customerAttributes); + $attributesToSave[$tableName] = $attributes + $customerAttributes; } } } @@ -584,13 +582,9 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $this->addRowError(self::ERROR_INVALID_STORE, $rowNumber); } // check password - if (isset( - $rowData['password'] - ) && strlen( - $rowData['password'] - ) && $this->string->strlen( - $rowData['password'] - ) < self::MIN_PASSWORD_LENGTH + if (isset($rowData['password']) + && strlen($rowData['password']) + && $this->string->strlen($rowData['password']) < self::MIN_PASSWORD_LENGTH ) { $this->addRowError(self::ERROR_PASSWORD_LENGTH, $rowNumber); } From 7b990ecd515ef9c033ed30e142324e5817dd2635 Mon Sep 17 00:00:00 2001 From: Aliaksei_Manenak <Aliaksei_Manenak@epam.com> Date: Tue, 28 May 2019 15:25:00 +0300 Subject: [PATCH 25/78] MAGETWO-59400: Invalid join condition in Product Flat Indexer - Fix sql queries. --- .../Indexer/Product/Flat/AbstractAction.php | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php index 3b63a90d4c3ae..ebad10e197622 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php @@ -169,8 +169,7 @@ protected function _reindex($storeId, array $changedIds = []) } /** - * Retrieve Product Type Instances - * as key - type code, value - instance model + * Retrieve Product Type Instances as key - type code, value - instance model * * @return array */ @@ -213,17 +212,19 @@ protected function _updateRelationProducts($storeId, $productIds = null) ) { $columns = $this->_productIndexerHelper->getFlatColumns(); $fieldList = array_keys($columns); - unset($columns['entity_id']); - unset($columns['child_id']); - unset($columns['is_child']); + unset( + $columns['entity_id'], + $columns['child_id'], + $columns['is_child'] + ); /** @var $select \Magento\Framework\DB\Select */ $select = $this->_connection->select()->from( ['t' => $this->_productIndexerHelper->getTable($relation->getTable())], - [$relation->getChildFieldName(), new \Zend_Db_Expr('1')] + ['entity_table.entity_id', $relation->getChildFieldName(), new \Zend_Db_Expr('1')] )->join( ['entity_table' => $this->_connection->getTableName('catalog_product_entity')], - 'entity_table.' . $metadata->getLinkField() . 't.' . $relation->getParentFieldName(), - [$relation->getParentFieldName() => 'entity_table.entity_id'] + "entity_table.{$metadata->getLinkField()} = t.{$relation->getParentFieldName()}", + [] )->join( ['e' => $this->_productIndexerHelper->getFlatTableName($storeId)], "e.entity_id = t.{$relation->getChildFieldName()}", @@ -232,10 +233,10 @@ protected function _updateRelationProducts($storeId, $productIds = null) if ($relation->getWhere() !== null) { $select->where($relation->getWhere()); } - if ($productIds !== null) { + if (!empty($productIds)) { $cond = [ $this->_connection->quoteInto("{$relation->getChildFieldName()} IN(?)", $productIds), - $this->_connection->quoteInto("entity_table.entity_id IN(?)", $productIds), + $this->_connection->quoteInto('entity_table.entity_id IN(?)', $productIds), ]; $select->where(implode(' OR ', $cond)); @@ -273,15 +274,11 @@ protected function _cleanRelationProducts($storeId) $select = $this->_connection->select()->distinct( true )->from( - ['t' => $this->_productIndexerHelper->getTable($relation->getTable())], - [] - )->join( - ['entity_table' => $this->_connection->getTableName('catalog_product_entity')], - 'entity_table.' . $metadata->getLinkField() . 't.' . $relation->getParentFieldName(), - [$relation->getParentFieldName() => 'entity_table.entity_id'] + $this->_productIndexerHelper->getTable($relation->getTable()), + $relation->getParentFieldName() ); $joinLeftCond = [ - "e.entity_id = entity_table.entity_id", + "e.{$metadata->getLinkField()} = t.{$relation->getParentFieldName()}", "e.child_id = t.{$relation->getChildFieldName()}", ]; if ($relation->getWhere() !== null) { @@ -302,7 +299,7 @@ protected function _cleanRelationProducts($storeId) 'e.is_child = ?', 1 )->where( - 'e.entity_id IN(?)', + "e.{$metadata->getLinkField()} IN(?)", $entitySelect )->where( "t.{$relation->getChildFieldName()} IS NULL" @@ -335,6 +332,8 @@ protected function _isFlatTableExists($storeId) } /** + * Get Metadata Pool + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() From f4098e2618d6a02d3fcc79564b94464d96da0fed Mon Sep 17 00:00:00 2001 From: vprohorov <prohorov.vital@gmail.com> Date: Tue, 28 May 2019 17:12:16 +0300 Subject: [PATCH 26/78] MAGETWO-91589: Slow query delete on sub SELECT query - Added integration test --- .../Model/CategoryUrlRewriteGeneratorTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php index cfa82b90e2f6a..1c2ee602c3bd4 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php @@ -8,7 +8,9 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ProductRepository; +use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -318,6 +320,53 @@ public function testGenerateUrlRewritesWithoutGenerateProductRewrites() $this->assertResults($productExpectedResult, $actualResults); } + /** + * Check number of records after removing product + * + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_products.php + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @return void + */ + public function testRemoveCatalogUrlRewrites() + { + /** @var CategoryRepository $categoryRepository */ + $categoryRepository = $this->objectManager->create(CategoryRepository::class); + $category = $categoryRepository->get(5); + $categoryId = $category->getId(); + + /** @var ProductRepository $productRepository */ + $productRepository = $this->objectManager->create(ProductRepository::class); + $product = $productRepository->get('12345'); + $productId = $product->getId(); + + $countBeforeRemoving = $this->getCountOfRewrites($productId, $categoryId); + $productRepository->delete($product); + $countAfterRemoving = $this->getCountOfRewrites($productId, $categoryId); + $this->assertEquals($countBeforeRemoving - 1, $countAfterRemoving); + } + + /** + * Get count of records in table + * + * @param $productId + * @param $categoryId + * @return string + */ + private function getCountOfRewrites($productId, $categoryId): string + { + /** @var Product $model */ + $model = $this->objectManager->get(Product::class); + $connection = $model->getConnection(); + $select = $connection->select(); + $select->from(Product::TABLE_NAME, 'COUNT(*)'); + $select->where('category_id = ?', $categoryId); + $select->where('product_id = ?', $productId); + return $connection->fetchOne($select); + } + /** * @param array $expected * @param array $actual From 17a806242d0e1730e7e15cb0f124410cb84be7cd Mon Sep 17 00:00:00 2001 From: vprohorov <prohorov.vital@gmail.com> Date: Tue, 28 May 2019 17:20:46 +0300 Subject: [PATCH 27/78] MAGETWO-63599: [GitHub] catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes #8204 - Added integration test --- .../Command/ImageResizeCommandTest.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php new file mode 100644 index 0000000000000..2cdfb5e7e4e6c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaStorage\Console\Command; + +use Magento\Catalog\Model\Product\Gallery\Processor; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Test for \Magento\MediaStorage\Console\Command\ImagesResizeCommand. + * + */ +class ImageResizeCommandTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CommandTester + */ + private $tester; + + /** + * @var ImagesResizeCommand + */ + private $command; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $fileName; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->fileName = 'image.jpg'; + $this->objectManager = Bootstrap::getObjectManager(); + $this->command = $this->objectManager->get(ImagesResizeCommand::class); + $this->tester = new CommandTester($this->command); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Test command with zero byte file + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_image.php + * + * @return void + */ + public function testExecuteWithZeroByteImage() + { + $this->mediaDirectory->writeFile($this->fileName, ''); + + /** @var ProductRepository $productRepository */ + $productRepository = $this->objectManager->create(ProductRepository::class); + $product = $productRepository->getById(1); + + /** @var Processor $mediaGalleryProcessor */ + $mediaGalleryProcessor = $this->objectManager->get(Processor::class); + $mediaGalleryProcessor->addImage( + $product, + $this->mediaDirectory->getAbsolutePath($this->fileName), + ['image','thumbnail','small_image'], + false, + false + ); + + $product->save(); + + $this->tester->execute([]); + $this->assertContains('Wrong file', $this->tester->getDisplay()); + } + + /** + * @inheritDoc + */ + public function tearDown() + { + $this->mediaDirectory->getDriver()->deleteFile($this->mediaDirectory->getAbsolutePath($this->fileName)); + } +} From df836321b12fa27baec764ab8091a9b5debb723e Mon Sep 17 00:00:00 2001 From: Mikalai Shostka <Mikalai_Shostka@epam.com> Date: Wed, 29 May 2019 17:00:29 +0300 Subject: [PATCH 28/78] MAGETWO-93061: CMS page of second website with same URLkey as first website, show content of First website instead of second website content. - Fix static test --- .../Model/StoreSwitcher/RewriteUrl.php | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index a4bca19cfc041..16e9c37ee4e52 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -63,10 +63,12 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s } $oldStoreId = $fromStore->getId(); - $oldRewrite = $this->urlFinder->findOneByData([ - UrlRewrite::REQUEST_PATH => $urlPath, - UrlRewrite::STORE_ID => $oldStoreId, - ]); + $oldRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $urlPath, + UrlRewrite::STORE_ID => $oldStoreId, + ] + ); if ($oldRewrite) { $targetUrl = $targetStore->getBaseUrl(); // look for url rewrite match on the target store @@ -75,13 +77,13 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s $targetUrl .= $currentRewrite->getRequestPath(); } } else { - $existingRewrite = $this->urlFinder->findOneByData([ - UrlRewrite::REQUEST_PATH => $urlPath - ]); - $currentRewrite = $this->urlFinder->findOneByData([ - UrlRewrite::REQUEST_PATH => $urlPath, - UrlRewrite::STORE_ID => $targetStore->getId(), - ]); + $existingRewrite = $this->urlFinder->findOneByData([UrlRewrite::REQUEST_PATH => $urlPath]); + $currentRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $urlPath, + UrlRewrite::STORE_ID => $targetStore->getId(), + ] + ); if ($existingRewrite && !$currentRewrite) { /** @var \Magento\Framework\App\Response\Http $response */ @@ -100,15 +102,19 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s */ private function findCurrentRewrite(UrlRewrite $oldRewrite, StoreInterface $targetStore) { - $currentRewrite = $this->urlFinder->findOneByData([ - UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), - UrlRewrite::STORE_ID => $targetStore->getId(), - ]); - if (!$currentRewrite) { - $currentRewrite = $this->urlFinder->findOneByData([ - UrlRewrite::REQUEST_PATH => $oldRewrite->getTargetPath(), + $currentRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), UrlRewrite::STORE_ID => $targetStore->getId(), - ]); + ] + ); + if (!$currentRewrite) { + $currentRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $oldRewrite->getTargetPath(), + UrlRewrite::STORE_ID => $targetStore->getId(), + ] + ); } return $currentRewrite; } From 47f615952a20493cc2c0995d69d9eb8b91eae29f Mon Sep 17 00:00:00 2001 From: vprohorov <prohorov.vital@gmail.com> Date: Thu, 30 May 2019 17:59:58 +0300 Subject: [PATCH 29/78] MAGETWO-63599: [GitHub] catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes #8204 - Static test fixes --- lib/internal/Magento/Framework/Image/Adapter/Gd2.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index d423fcfc6773a..df6c7652758c3 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -324,10 +324,12 @@ public function checkAlpha($fileName) * Checks if image has alpha transparency * * @param resource $imageResource - * @param int $fileType one of the constants IMAGETYPE_* + * @param int $fileType * @param bool &$isAlpha * @param bool &$isTrueColor + * * @return boolean + * * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ private function _getTransparency($imageResource, $fileType, &$isAlpha = false, &$isTrueColor = false) From 3602575d7d79ae757f6e71591e7e2a9636750767 Mon Sep 17 00:00:00 2001 From: Lilit Sargsyan <Lilit_Sargsyan@epam.com> Date: Fri, 31 May 2019 11:58:13 +0400 Subject: [PATCH 30/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values - Updated automated test script. --- ...nCheckConfigurableProductAttributeValueUniquenessTest.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index 61cce181efe61..28d594dd072cc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -11,9 +11,8 @@ <test name="AdminCheckConfigurableProductAttributeValueUniquenessTest"> <annotations> <features value="ConfigurableProduct"/> - <stories value="Edit a configurable product in admin"/> - <title value="Check attribute value uniqueness in configurable products"/> - <description value="Check attribute value uniqueness in configurable products"/> + <title value="Attribute value validation (check for uniqueness) in configurable products"/> + <description value="Attribute value validation (check for uniqueness) in configurable products"/> <severity value="MAJOR"/> <testCaseId value="MAGETWO-99519"/> <useCaseId value="MAGETWO-99443"/> From a592d46fb6b101ef65bf0f2a9b2b99e97d52430f Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Mon, 3 Jun 2019 15:09:54 +0300 Subject: [PATCH 31/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../Product/Attribute/CreateOptions.php | 6 ++-- .../js/variations/steps/attributes_values.js | 34 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php index 2788a4947aa46..8b4c8d29cd98c 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php @@ -115,15 +115,15 @@ private function checkUnique(ProductAttributeInterface $attribute, array $newOpt $allOptions = array_merge($originalOptions, $newOptions); $optionValues = array_map( function ($option) { - return $option['label']; + return $option['label'] ?? null; }, $allOptions ); - $uniqueValues = array_unique($optionValues); + $uniqueValues = array_unique(array_filter($optionValues)); $duplicates = array_diff_assoc($optionValues, $uniqueValues); if ($duplicates) { - throw new LocalizedException(__('Attributes must have unique option values')); + throw new LocalizedException(__('The value of attribute ""%1"" must be unique', $attribute->getName())); } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index 0287d10b247ed..b470592529e8f 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -148,22 +148,22 @@ define([ saveAttribute: function () { var errorMessage = $.mage.__('Select options for all attributes or remove unused attributes.'); - this.attributes.each(function (attribute) { + if (!this.attributes().length) { + throw new Error(errorMessage); + } + + _.each(this.attributes(), function (attribute) { attribute.chosen = []; if (!attribute.chosenOptions.getLength()) { throw new Error(errorMessage); } - attribute.chosenOptions.each(function (id) { + _.each(attribute.chosenOptions(), function (id) { attribute.chosen.push(attribute.options.findWhere({ id: id })); }); }); - - if (!this.attributes().length) { - throw new Error(errorMessage); - } }, /** @@ -188,10 +188,10 @@ define([ allOptions = [], attributesWithDuplicateOptions = []; - this.attributes.each(function (attribute) { + _.each(this.attributes(), function (attribute) { allOptions[attribute.id] = []; - attribute.options.each(function (element) { + _.each(attribute.options(), function (element) { var option = attribute.options.findWhere({ id: element.id }); @@ -200,16 +200,22 @@ define([ newOptions.push(option); } - if (typeof allOptions[attribute.id][option.label] !== 'undefined') { - attributesWithDuplicateOptions.push(attribute); - } + if (!_.isUndefined(option.label)) { + if (!_.isUndefined(allOptions[attribute.id][option.label])) { + attributesWithDuplicateOptions.push(attribute); + } - allOptions[attribute.id][option.label] = option.label; + allOptions[attribute.id][option.label] = option.label; + } }); }); if (attributesWithDuplicateOptions.length) { - throw new Error($.mage.__('Attributes must have unique option values')); + _.each(attributesWithDuplicateOptions, function (attribute) { + throw new Error($.mage.__( + 'The value of attribute ""%1"" must be unique').replace("\"%1\"", attribute.label) + ); + }); } if (!newOptions.length) { @@ -231,7 +237,7 @@ define([ return; } - this.attributes.each(function (attribute) { + _.each(this.attributes(), function (attribute) { _.each(savedOptions, function (newOptionId, oldOptionId) { var option = attribute.options.findWhere({ id: oldOptionId From 72841f206f627e1369069e097103d172f33a732f Mon Sep 17 00:00:00 2001 From: Lusine Papyan <Lusine_Papyan@epam.com> Date: Fri, 24 May 2019 10:12:40 +0400 Subject: [PATCH 32/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Updated automated test script --- .../ActionGroup/AdminProductActionGroup.xml | 9 ++++++++ ...ctedUserAddCategoryFromProductPageTest.xml | 22 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index da570f9ed99b0..adae8cb416907 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -428,6 +428,15 @@ </arguments> <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="searchAndSelectCategory"/> </actionGroup> + <!--Remove category from product in ProductFrom Page--> + <actionGroup name="removeCategoryFromProduct"> + <arguments> + <argument name="categoryName" type="string" defaultValue="{{_defaultCategory.name}}"/> + </arguments> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <click selector="{{AdminProductFormSection.unselectCategories(categoryName)}}" stepKey="unselectCategories"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategory"/> + </actionGroup> <actionGroup name="expandAdminProductSection"> <arguments> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index 55a7a7632dea6..ab74d131cbb58 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -74,8 +74,8 @@ <!--Log out of admin and login with newly created user--> <comment userInput="Log out of admin and login with newly created user" stepKey="commentLoginWithNewUser"/> <actionGroup ref="logout" stepKey="logoutOfAdmin"/> - <actionGroup ref="LoginNewUser" stepKey="loginActionGroup"> - <argument name="user" value="admin2"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsNewUser"> + <argument name="adminUser" value="admin2"/> </actionGroup> <!--Go to create product page--> <comment userInput="Go to create product page" stepKey="commentGoCreateProductPage"/> @@ -88,5 +88,23 @@ <argument name="categoryName" value="$$createCategory.name$$"/> </actionGroup> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Assert that category exist in field--> + <comment userInput="Assert that category exist in field" stepKey="commentAssertion"/> + <grabTextFrom selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="grabCategoryName"/> + <assertContains stepKey="assertThatCategory"> + <expectedResult type="variable">$$createCategory.name$$</expectedResult> + <actualResult type="variable">$grabCategoryName</actualResult> + </assertContains> + <!--Remove the category from the product and assert that it removed--> + <comment userInput="Remove the category from the product and assert that it removed" stepKey="AssertCategoryRemoved"/> + <actionGroup ref="removeCategoryFromProduct" stepKey="removeCategoryFromProduct"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductAfterRemovingCategory"/> + <grabTextFrom selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="grabCategoryFieldContent"/> + <assertNotContains stepKey="assertThatCategoryRemoved"> + <expectedResult type="variable">$$createCategory.name$$</expectedResult> + <actualResult type="variable">$grabCategoryFieldContent</actualResult> + </assertNotContains> </test> </tests> From 181e34b53435691954e639079477429ab56ee270 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina <veronika_kurochkina@epam.com> Date: Tue, 4 Jun 2019 14:29:46 +0300 Subject: [PATCH 33/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Fix static --- .../Product/Form/Modifier/CategoriesTest.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index d35aaa72a2e97..932b09f7df9cb 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -97,12 +97,15 @@ protected function setUp() */ protected function createModel() { - return $this->objectManager->getObject(Categories::class, [ - 'locator' => $this->locatorMock, - 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, - 'arrayManager' => $this->arrayManagerMock, - 'authorization' => $this->authorizationMock - ]); + return $this->objectManager->getObject( + Categories::class, + [ + 'locator' => $this->locatorMock, + 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, + 'arrayManager' => $this->arrayManagerMock, + 'authorization' => $this->authorizationMock + ] + ); } public function testModifyData() From 6779af6c78888590f962bd8be75ec313ae0038b0 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Wed, 5 Jun 2019 13:29:06 +0300 Subject: [PATCH 34/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../attribute/steps/attributes_values.phtml | 2 +- .../js/variations/steps/attributes_values.js | 54 +++++++++++-------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml index cc25474049190..1bf5a7ff7ee07 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml @@ -74,7 +74,7 @@ <label data-bind="text: label, visible: label, attr:{for:id}" class="admin__field-label"></label> </div> - <div class="admin__field admin__field-create-new" data-bind="visible: !label"> + <div class="admin__field admin__field-create-new" data-bind="attr:{'data-role':id}, visible: !label"> <div class="admin__field-control"> <input class="admin__control-text" name="label" diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index b470592529e8f..c0eb67cf0ccea 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -90,13 +90,42 @@ define([ * @param {Object} option */ saveOption: function (option) { - if (!_.isEmpty(option.label)) { + if (this.isValidOption(option)) { this.options.remove(option); this.options.push(option); this.chosenOptions.push(option.id); } }, + /** + * @param {Object} option + * @return boolean + */ + isValidOption: function (option) { + var duplicatedOptions = [], + errorOption, + allOptions = []; + + if (_.isEmpty(option.label)) { + return false; + } + + _.each(this.options(), function (option) { + if (!_.isUndefined(allOptions[option.label])) { + duplicatedOptions.push(option); + } + + allOptions[option.label] = option.label; + }); + + if (duplicatedOptions.length) { + errorOption = $("[data-role=\"" + option.id + "\""); + errorOption.addClass("_error"); + return false; + } + return true; + }, + /** * @param {Object} option */ @@ -128,6 +157,7 @@ define([ })); attribute.opened = ko.observable(this.initialOpened(index)); attribute.collapsible = ko.observable(true); + attribute.isValidOption = this.isValidOption; return attribute; }, @@ -184,13 +214,9 @@ define([ * @return {Boolean} */ saveOptions: function () { - var newOptions = [], - allOptions = [], - attributesWithDuplicateOptions = []; + var newOptions = []; _.each(this.attributes(), function (attribute) { - allOptions[attribute.id] = []; - _.each(attribute.options(), function (element) { var option = attribute.options.findWhere({ id: element.id @@ -199,25 +225,9 @@ define([ if (option['is_new'] === true) { newOptions.push(option); } - - if (!_.isUndefined(option.label)) { - if (!_.isUndefined(allOptions[attribute.id][option.label])) { - attributesWithDuplicateOptions.push(attribute); - } - - allOptions[attribute.id][option.label] = option.label; - } }); }); - if (attributesWithDuplicateOptions.length) { - _.each(attributesWithDuplicateOptions, function (attribute) { - throw new Error($.mage.__( - 'The value of attribute ""%1"" must be unique').replace("\"%1\"", attribute.label) - ); - }); - } - if (!newOptions.length) { return false; } From da5ddf57b71ea06137ddc3ee5b49e2b909f74934 Mon Sep 17 00:00:00 2001 From: Aliaksei_Manenak <Aliaksei_Manenak@epam.com> Date: Fri, 7 Jun 2019 10:41:46 +0300 Subject: [PATCH 35/78] MAGETWO-59400: Invalid join condition in Product Flat Indexer - Add integration test. --- .../Indexer/Product/Flat/Action/Full.php | 22 +++ .../Product/Flat/Action/RelationTest.php | 134 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php new file mode 100644 index 0000000000000..17ffb5cf2748a --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Indexer\Product\Flat\Action; + +/** + * Class Full reindex action + */ +class Full extends \Magento\Catalog\Model\Indexer\Product\Flat\Action\Full +{ + /** + * List of product types available in installation + * + * @var array + */ + protected $_productTypes; +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php new file mode 100644 index 0000000000000..85bcd54767e36 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Catalog\Model\Indexer\Product\Flat\Action\Full as FlatIndexerFull; + +/** + * Test relation customization + */ +class RelationTest extends \Magento\TestFramework\Indexer\TestCase +{ + /** + * @var FlatIndexerFull + */ + private $indexer; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * Updated flat tables + * + * @var array + */ + private $flatUpdated = []; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $tableBuilderMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder::class); + $flatTableBuilderMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class); + + $productIndexerHelper = $objectManager->create( + \Magento\Catalog\Helper\Product\Flat\Indexer::class, + ['addChildData' => 1] + ); + $this->indexer = $objectManager->create( + FlatIndexerFull::class, + [ + 'productHelper' => $productIndexerHelper, + 'tableBuilder' => $tableBuilderMock, + 'flatTableBuilder' => $flatTableBuilderMock + ] + ); + $this->storeManager = $objectManager->create(StoreManagerInterface::class); + $this->connection = $objectManager->get(ResourceConnection::class)->getConnection(); + + foreach ($this->storeManager->getStores() as $store) { + $flatTable = $productIndexerHelper->getFlatTableName($store->getId()); + if ($this->connection->isTableExists($flatTable) && + !$this->connection->tableColumnExists($flatTable, 'child_id') && + !$this->connection->tableColumnExists($flatTable, 'is_child') + ) { + $this->connection->addColumn( + $flatTable, + 'child_id', + [ + 'type' => 'integer', + 'length' => null, + 'unsigned' => true, + 'nullable' => true, + 'default' => null, + 'unique' => true, + 'comment' => 'Child Id', + ] + ); + $this->connection->addColumn( + $flatTable, + 'is_child', + [ + 'type' => 'smallint', + 'length' => 1, + 'unsigned' => true, + 'nullable' => false, + 'default' => '0', + 'comment' => 'Checks If Entity Is Child', + ] + ); + + $this->flatUpdated[] = $flatTable; + } + } + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + foreach ($this->flatUpdated as $flatTable) { + $this->connection->dropColumn($flatTable, 'child_id'); + $this->connection->dropColumn($flatTable, 'is_child'); + } + } + + /** + * Test that SQL generated for relation customization is valid + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Exception + */ + public function testExecute() : void + { + try { + $this->indexer->execute(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + if ($e->getPrevious() instanceof \Zend_Db_Statement_Exception) { + $this->fail($e->getMessage()); + } + throw $e; + } + } +} From 9a988b585aaf674cacc2038e24a1e40013d08843 Mon Sep 17 00:00:00 2001 From: Mikalai Shostka <mikalai_shostka@epam.com> Date: Fri, 7 Jun 2019 17:22:46 +0300 Subject: [PATCH 36/78] MAGETWO-93061: CMS page of second website with same URLkey as first website, show content of First website instead of second website content. - Fix CR comments --- .../Page/Grid/Renderer/Action/UrlBuilder.php | 65 +--------- .../Component/Listing/Column/PageActions.php | 13 +- .../Cms/ViewModel/Page/Grid/UrlBuilder.php | 112 ++++++++++++++++++ app/code/Magento/Cms/etc/adminhtml/di.xml | 5 + 4 files changed, 132 insertions(+), 63 deletions(-) create mode 100644 app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php index 8bff494ef79f3..08ba2c3fff330 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php @@ -3,15 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - namespace Magento\Cms\Block\Adminhtml\Page\Grid\Renderer\Action; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Url\EncoderInterface; -use Magento\Framework\App\ActionInterface; -use Magento\Store\Model\StoreManagerInterface; - /** * Url builder class used to compose dynamic urls. */ @@ -22,31 +15,12 @@ class UrlBuilder */ protected $frontendUrlBuilder; - /** - * @var EncoderInterface - */ - private $urlEncoder; - - /** - * @var StoreManagerInterface - */ - private $storeManager; - /** * @param \Magento\Framework\UrlInterface $frontendUrlBuilder - * @param EncoderInterface|null $urlEncoder - * @param StoreManagerInterface|null $storeManager */ - public function __construct( - \Magento\Framework\UrlInterface $frontendUrlBuilder, - EncoderInterface $urlEncoder = null, - StoreManagerInterface $storeManager = null - ) { + public function __construct(\Magento\Framework\UrlInterface $frontendUrlBuilder) + { $this->frontendUrlBuilder = $frontendUrlBuilder; - $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance() - ->get(EncoderInterface::class); - $this->storeManager = $storeManager?: ObjectManager::getInstance() - ->get(StoreManagerInterface::class); } /** @@ -61,22 +35,12 @@ public function getUrl($routePath, $scope, $store) { if ($scope) { $this->frontendUrlBuilder->setScope($scope); - $targetUrl = $this->frontendUrlBuilder->getUrl( - $routePath, - [ - '_current' => false, - '_nosid' => true, - '_query' => [ - StoreManagerInterface::PARAM_NAME => $store - ] - ] - ); $href = $this->frontendUrlBuilder->getUrl( - 'stores/store/switch', + $routePath, [ '_current' => false, '_nosid' => true, - '_query' => $this->prepareRequestQuery($store, $targetUrl) + '_query' => [\Magento\Store\Model\StoreManagerInterface::PARAM_NAME => $store] ] ); } else { @@ -91,25 +55,4 @@ public function getUrl($routePath, $scope, $store) return $href; } - - /** - * Prepare request query - * - * @param string $store - * @param string $href - * @return array - */ - private function prepareRequestQuery(string $store, string $href) : array - { - $storeView = $this->storeManager->getDefaultStoreView(); - $query = [ - StoreManagerInterface::PARAM_NAME => $store, - ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($href) - ]; - if ($storeView->getCode() !== $store) { - $query['___from_store'] = $storeView->getCode(); - } - - return $query; - } } diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php index 26d31456bf61d..9c57aa050b01b 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -27,6 +27,11 @@ class PageActions extends Column */ protected $actionUrlBuilder; + /** + * @var \Magento\Cms\ViewModel\Page\Grid\UrlBuilder + */ + private $scopeUrlBuilder; + /** * @var \Magento\Framework\UrlInterface */ @@ -50,6 +55,7 @@ class PageActions extends Column * @param array $components * @param array $data * @param string $editUrl + * @param \Magento\Cms\ViewModel\Page\Grid\UrlBuilder|null $scopeUrlBuilder */ public function __construct( ContextInterface $context, @@ -58,12 +64,15 @@ public function __construct( UrlInterface $urlBuilder, array $components = [], array $data = [], - $editUrl = self::CMS_URL_PATH_EDIT + $editUrl = self::CMS_URL_PATH_EDIT, + \Magento\Cms\ViewModel\Page\Grid\UrlBuilder $scopeUrlBuilder = null ) { $this->urlBuilder = $urlBuilder; $this->actionUrlBuilder = $actionUrlBuilder; $this->editUrl = $editUrl; parent::__construct($context, $uiComponentFactory, $components, $data); + $this->scopeUrlBuilder = $scopeUrlBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Cms\ViewModel\Page\Grid\UrlBuilder::class); } /** @@ -92,7 +101,7 @@ public function prepareDataSource(array $dataSource) } if (isset($item['identifier'])) { $item[$name]['preview'] = [ - 'href' => $this->actionUrlBuilder->getUrl( + 'href' => $this->scopeUrlBuilder->getUrl( $item['identifier'], isset($item['_first_store_id']) ? $item['_first_store_id'] : null, isset($item['store_code']) ? $item['store_code'] : null diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php new file mode 100644 index 0000000000000..b39a770a446e9 --- /dev/null +++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\ViewModel\Page\Grid; + +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\App\ActionInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Url builder class used to compose dynamic urls. + */ +class UrlBuilder +{ + /** + * @var \Magento\Framework\UrlInterface + */ + protected $frontendUrlBuilder; + + /** + * @var EncoderInterface + */ + private $urlEncoder; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param \Magento\Framework\UrlInterface $frontendUrlBuilder + * @param EncoderInterface $urlEncoder + * @param StoreManagerInterface $storeManager + */ + public function __construct( + \Magento\Framework\UrlInterface $frontendUrlBuilder, + EncoderInterface $urlEncoder, + StoreManagerInterface $storeManager + ) { + $this->frontendUrlBuilder = $frontendUrlBuilder; + $this->urlEncoder = $urlEncoder; + $this->storeManager = $storeManager; + } + + /** + * Get action url + * + * @param string $routePath + * @param string $scope + * @param string $store + * @return string + */ + public function getUrl($routePath, $scope, $store) + { + if ($scope) { + $this->frontendUrlBuilder->setScope($scope); + $targetUrl = $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $store + ] + ] + ); + $href = $this->frontendUrlBuilder->getUrl( + 'stores/store/switch', + [ + '_current' => false, + '_nosid' => true, + '_query' => $this->prepareRequestQuery($store, $targetUrl) + ] + ); + } else { + $href = $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true + ] + ); + } + + return $href; + } + + /** + * Prepare request query + * + * @param string $store + * @param string $href + * @return array + */ + private function prepareRequestQuery(string $store, string $href) : array + { + $storeView = $this->storeManager->getDefaultStoreView(); + $query = [ + StoreManagerInterface::PARAM_NAME => $store, + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($href) + ]; + if ($storeView->getCode() !== $store) { + $query['___from_store'] = $storeView->getCode(); + } + + return $query; + } +} diff --git a/app/code/Magento/Cms/etc/adminhtml/di.xml b/app/code/Magento/Cms/etc/adminhtml/di.xml index 98a8ff6e9ec91..363217af4cd07 100644 --- a/app/code/Magento/Cms/etc/adminhtml/di.xml +++ b/app/code/Magento/Cms/etc/adminhtml/di.xml @@ -12,6 +12,11 @@ <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> </arguments> </type> + <type name="Magento\Cms\ViewModel\Page\Grid\UrlBuilder"> + <arguments> + <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> + </arguments> + </type> <type name="Magento\Cms\Model\Wysiwyg\CompositeConfigProvider"> <arguments> <argument name="variablePluginConfigProvider" xsi:type="array"> From ea66bb32cf0d58f6104e9e35f60cdd5d091b51ba Mon Sep 17 00:00:00 2001 From: Lusine Papyan <Lusine_Papyan@epam.com> Date: Tue, 30 Apr 2019 12:08:39 +0400 Subject: [PATCH 37/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Updated automated test script --- .../Mftf/ActionGroup/AdminUserActionGroup.xml | 14 ------------- .../ActionGroup/SwitchAccountActionGroup.xml | 11 ---------- .../AdminDeleteCreatedUserActionGroup.xml | 14 +++++++++++++ .../Mftf/ActionGroup/AdminUserActionGroup.xml | 20 +++++++++++++++++++ .../Section/AdminEditRoleResourcesSection.xml | 1 - 5 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml index 841dfb5607cd1..d530f8e9f68b0 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml @@ -38,18 +38,4 @@ <waitForPageLoad stepKey="waitForSaveUser" time="10"/> <see userInput="You saved the user." stepKey="seeSuccessMessage" /> </actionGroup> - <!--Delete User--> - <actionGroup name="AdminDeleteNewUserActionGroup"> - <arguments> - <argument name="userName" type="string" defaultValue="John"/> - </arguments> - <click stepKey="clickOnUser" selector="{{AdminDeleteUserSection.theUser(userName)}}"/> - <fillField stepKey="typeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> - <scrollToTopOfPage stepKey="scrollToTop"/> - <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> - <waitForPageLoad stepKey="waitForDeletePopupOpen" time="5"/> - <click stepKey="clickToConfirm" selector="{{AdminDeleteUserSection.confirm}}"/> - <waitForPageLoad stepKey="waitForPageLoad" time="10"/> - <see userInput="You deleted the user." stepKey="seeSuccessMessage" /> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml index 40bf68db6ea68..85e23940e1409 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml @@ -16,15 +16,4 @@ <see userInput="You have logged out." stepKey="seeSuccessMessage" /> <waitForElementVisible selector="//*[@data-ui-id='messages-message-success']" stepKey="waitForSuccessMessageLoggedOut" time="5"/> </actionGroup> - - <!--Login New User--> - <actionGroup name="LoginNewUser"> - <arguments> - <argument name="user" defaultValue="NewAdmin"/> - </arguments> - <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}" stepKey="navigateToAdmin"/> - <fillField userInput="{{user.username}}" selector="{{LoginFormSection.username}}" stepKey="fillUsername"/> - <fillField userInput="{{user.password}}" selector="{{LoginFormSection.password}}" stepKey="fillPassword"/> - <click selector="{{LoginFormSection.signIn}}" stepKey="clickLogin"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml index 74124f366a54b..8a1abc1ca1622 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml @@ -21,4 +21,18 @@ <click stepKey="clickToConfirm" selector="{{AdminDeleteUserSection.confirm}}"/> <see stepKey="seeDeleteMessageForUser" userInput="You deleted the user."/> </actionGroup> + <!--Delete User--> + <actionGroup name="AdminDeleteNewUserActionGroup"> + <arguments> + <argument name="userName" type="string" defaultValue="John"/> + </arguments> + <click stepKey="clickOnUser" selector="{{AdminDeleteUserSection.theUser(userName)}}"/> + <fillField stepKey="typeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> + <waitForPageLoad stepKey="waitForDeletePopupOpen" time="5"/> + <click stepKey="clickToConfirm" selector="{{AdminDeleteUserSection.confirm}}"/> + <waitForPageLoad stepKey="waitForPageLoad" time="10"/> + <see userInput="You deleted the user." stepKey="seeSuccessMessage" /> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml new file mode 100644 index 0000000000000..79c3b04694326 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.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"> + <!--Login New User--> + <actionGroup name="LoginNewUser"> + <arguments> + <argument name="user" defaultValue="NewAdmin"/> + </arguments> + <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}" stepKey="navigateToAdmin"/> + <fillField userInput="{{user.username}}" selector="{{LoginFormSection.username}}" stepKey="fillUsername"/> + <fillField userInput="{{user.password}}" selector="{{LoginFormSection.password}}" stepKey="fillPassword"/> + <click selector="{{LoginFormSection.signIn}}" stepKey="clickLogin"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml index 6bcfe48addb43..48873bd9d152e 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml @@ -7,7 +7,6 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEditRoleResourcesSection"> - <element name="roleScopes" type="select" selector="#gws_is_all"/> <element name="resourceAccess" type="select" selector="#all"/> <element name="resources" type="checkbox" selector="#role_info_tabs_account"/> <element name="storeName" type="checkbox" selector="//label[contains(text(),'{{var1}}')]" parameterized="true"/> From 896db5f2555a674709fa8b7fe7b79f6d7b436b86 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Mon, 10 Jun 2019 09:40:42 +0300 Subject: [PATCH 38/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../adminhtml/web/js/variations/steps/attributes_values.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index c0eb67cf0ccea..9a2ef138919bc 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -223,6 +223,12 @@ define([ }); if (option['is_new'] === true) { + if (!attribute.isValidOption(option)) { + throw new Error($.mage.__( + 'The value of attribute ""%1"" must be unique').replace("\"%1\"", attribute.label) + ); + } + newOptions.push(option); } }); From 07c03a6be9f42ba1905ed0021e4bf768d98b0ce9 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina <veronika_kurochkina@epam.com> Date: Mon, 10 Jun 2019 15:11:36 +0300 Subject: [PATCH 39/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Updated automated test script --- .../Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index ab74d131cbb58..c084dd78b71be 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -96,7 +96,7 @@ <actualResult type="variable">$grabCategoryName</actualResult> </assertContains> <!--Remove the category from the product and assert that it removed--> - <comment userInput="Remove the category from the product and assert that it removed" stepKey="AssertCategoryRemoved"/> + <comment userInput="Remove the category from the product and assert that it removed" stepKey="assertCategoryRemoved"/> <actionGroup ref="removeCategoryFromProduct" stepKey="removeCategoryFromProduct"> <argument name="categoryName" value="$$createCategory.name$$"/> </actionGroup> From 346edaccad06670af34b7302459c806b4e39fe39 Mon Sep 17 00:00:00 2001 From: natalia <natalia_marozava@epam.com> Date: Mon, 10 Jun 2019 14:47:57 +0300 Subject: [PATCH 40/78] MAGETWO-63599: Check of image size is moved to _canProcess() method --- .../Magento/Framework/Image/Adapter/AbstractAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php index 00f2c39aea8a1..42f7e42960eff 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php +++ b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php @@ -307,7 +307,7 @@ public function getImageType() if ($this->_fileType) { return $this->_fileType; } else { - if ($this->_canProcess() && filesize($this->_fileName) > 0) { + if ($this->_canProcess()) { list($this->_imageSrcWidth, $this->_imageSrcHeight, $this->_fileType) = getimagesize($this->_fileName); return $this->_fileType; } @@ -709,7 +709,7 @@ protected function _prepareDestination($destination = null, $newName = null) */ protected function _canProcess() { - return !empty($this->_fileName); + return !empty($this->_fileName) && filesize($this->_fileName) > 0; } /** From 15d6d741a88becd9cda725b7fd58edc900f0af3b Mon Sep 17 00:00:00 2001 From: Aliaksei Yakimovich2 <aliaksei_yakimovich2@epam.com> Date: Tue, 11 Jun 2019 17:10:16 +0300 Subject: [PATCH 41/78] MAGETWO-60918: Fatal error on Import/Export page if deleted category ids exists in category path - Fixed an import/exposrt issue; --- .../CatalogImportExport/Model/Export/Product.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 75249e4907862..428c61c7fec0f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -444,8 +444,11 @@ protected function initCategories() if ($pathSize > 1) { $path = []; for ($i = 1; $i < $pathSize; $i++) { - $name = $collection->getItemById($structure[$i])->getName(); - $path[] = $this->quoteCategoryDelimiter($name); + $childCategory = $collection->getItemById($structure[$i]); + if ($childCategory) { + $name = $childCategory->getName(); + $path[] = $this->quoteCategoryDelimiter($name); + } } $this->_rootCategories[$category->getId()] = array_shift($path); if ($pathSize > 2) { @@ -673,8 +676,8 @@ protected function prepareLinks(array $productIds) /** * Update data row with information about categories. Return true, if data row was updated * - * @param array &$dataRow - * @param array &$rowCategories + * @param array $dataRow + * @param array $rowCategories * @param int $productId * @return bool */ @@ -840,6 +843,7 @@ protected function paginateCollection($page, $pageSize) public function export() { //Execution time may be very long + // phpcs:ignore Magento2.Functions.DiscouragedFunction set_time_limit(0); $writer = $this->getWriter(); @@ -963,6 +967,7 @@ protected function loadCollection(): array * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ protected function collectRawData() { @@ -1057,6 +1062,7 @@ protected function collectRawData() return $data; } + //phpcs:enable Generic.Metrics.NestingLevel /** * Wrap values with double quotes if "Fields Enclosure" option is enabled From d122eec4a9c404f729257ca8675bf5142bc503ab Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Tue, 11 Jun 2019 13:12:54 -0500 Subject: [PATCH 42/78] MAGETWO-99736: Authorize.net - 3D Secure 2.0 Support for 2.3 --- .../Gateway/Request/StubDataBuilder.php | 28 +++ .../Unit/Observer/DataAssignObserverTest.php | 7 +- .../AuthorizenetAcceptjs/etc/adminhtml/di.xml | 20 ++ .../Magento/AuthorizenetAcceptjs/etc/di.xml | 1 + .../Request/Authorize3DSecureBuilder.php | 82 +++++++ .../Magento/AuthorizenetCardinal/LICENSE.txt | 48 +++++ .../AuthorizenetCardinal/LICENSE_AFL.txt | 48 +++++ .../Model/Checkout/ConfigProvider.php | 45 ++++ .../AuthorizenetCardinal/Model/Config.php | 47 ++++ .../Observer/DataAssignObserver.php | 63 ++++++ .../Magento/AuthorizenetCardinal/README.md | 1 + .../Unit/Observer/DataAssignObserverTest.php | 100 +++++++++ .../AuthorizenetCardinal/composer.json | 30 +++ .../etc/adminhtml/system.xml | 22 ++ .../AuthorizenetCardinal/etc/config.xml | 16 ++ .../Magento/AuthorizenetCardinal/etc/di.xml | 16 ++ .../AuthorizenetCardinal/etc/events.xml | 13 ++ .../AuthorizenetCardinal/etc/frontend/di.xml | 18 ++ .../AuthorizenetCardinal/etc/module.xml | 15 ++ .../AuthorizenetCardinal/registration.php | 9 + .../view/frontend/requirejs-config.js | 15 ++ .../web/js/authorizenet-accept-mixin.js | 70 ++++++ app/code/Magento/CardinalCommerce/LICENSE.txt | 48 +++++ .../Magento/CardinalCommerce/LICENSE_AFL.txt | 48 +++++ .../Model/Adminhtml/Source/Environment.php | 36 ++++ .../Model/Checkout/ConfigProvider.php | 53 +++++ .../Magento/CardinalCommerce/Model/Config.php | 118 ++++++++++ .../CardinalCommerce/Model/JwtManagement.php | 141 ++++++++++++ .../Model/Request/TokenBuilder.php | 99 +++++++++ .../Model/Response/JwtParser.php | 120 +++++++++++ .../Model/Response/JwtPayloadValidator.php | 132 ++++++++++++ .../Response/JwtPayloadValidatorInterface.php | 21 ++ app/code/Magento/CardinalCommerce/README.md | 1 + .../Test/Unit/JwtManagementTest.php | 173 +++++++++++++++ .../Response/JwtPayloadValidatorTest.php | 202 ++++++++++++++++++ .../Magento/CardinalCommerce/composer.json | 27 +++ .../CardinalCommerce/etc/adminhtml/system.xml | 48 +++++ .../Magento/CardinalCommerce/etc/config.xml | 20 ++ app/code/Magento/CardinalCommerce/etc/di.xml | 10 + .../CardinalCommerce/etc/frontend/di.xml | 18 ++ .../Magento/CardinalCommerce/etc/module.xml | 15 ++ .../Magento/CardinalCommerce/registration.php | 9 + .../view/frontend/requirejs-config.js | 20 ++ .../view/frontend/web/js/cardinal-client.js | 131 ++++++++++++ .../view/frontend/web/js/cardinal-factory.js | 29 +++ composer.json | 2 + composer.lock | 4 +- .../Fixture/expected_request/authorize.php | 70 ++++++ .../Fixture/expected_request/sale.php | 70 ++++++ .../Fixture/full_order_with_3dsecure.php | 21 ++ .../Fixture/response/authorize.php | 47 ++++ .../Fixture/response/cardinal_jwt.php | 48 +++++ .../Gateway/Command/AuthorizeCommandTest.php | 186 ++++++++++++++++ .../Gateway/Command/SaleCommandTest.php | 85 ++++++++ ...ndleWithCatalogPriceRuleCalculatorTest.php | 1 + 55 files changed, 2762 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php create mode 100644 app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php create mode 100644 app/code/Magento/AuthorizenetCardinal/LICENSE.txt create mode 100644 app/code/Magento/AuthorizenetCardinal/LICENSE_AFL.txt create mode 100644 app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php create mode 100644 app/code/Magento/AuthorizenetCardinal/Model/Config.php create mode 100644 app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php create mode 100644 app/code/Magento/AuthorizenetCardinal/README.md create mode 100644 app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php create mode 100644 app/code/Magento/AuthorizenetCardinal/composer.json create mode 100644 app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml create mode 100644 app/code/Magento/AuthorizenetCardinal/etc/config.xml create mode 100644 app/code/Magento/AuthorizenetCardinal/etc/di.xml create mode 100644 app/code/Magento/AuthorizenetCardinal/etc/events.xml create mode 100644 app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml create mode 100644 app/code/Magento/AuthorizenetCardinal/etc/module.xml create mode 100644 app/code/Magento/AuthorizenetCardinal/registration.php create mode 100644 app/code/Magento/AuthorizenetCardinal/view/frontend/requirejs-config.js create mode 100644 app/code/Magento/AuthorizenetCardinal/view/frontend/web/js/authorizenet-accept-mixin.js create mode 100644 app/code/Magento/CardinalCommerce/LICENSE.txt create mode 100644 app/code/Magento/CardinalCommerce/LICENSE_AFL.txt create mode 100644 app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php create mode 100644 app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php create mode 100644 app/code/Magento/CardinalCommerce/Model/Config.php create mode 100644 app/code/Magento/CardinalCommerce/Model/JwtManagement.php create mode 100644 app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php create mode 100644 app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php create mode 100644 app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php create mode 100644 app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php create mode 100644 app/code/Magento/CardinalCommerce/README.md create mode 100644 app/code/Magento/CardinalCommerce/Test/Unit/JwtManagementTest.php create mode 100644 app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php create mode 100644 app/code/Magento/CardinalCommerce/composer.json create mode 100644 app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml create mode 100644 app/code/Magento/CardinalCommerce/etc/config.xml create mode 100644 app/code/Magento/CardinalCommerce/etc/di.xml create mode 100644 app/code/Magento/CardinalCommerce/etc/frontend/di.xml create mode 100644 app/code/Magento/CardinalCommerce/etc/module.xml create mode 100644 app/code/Magento/CardinalCommerce/registration.php create mode 100644 app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js create mode 100644 app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js create mode 100644 app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/authorize.php create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/sale.php create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/full_order_with_3dsecure.php create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/authorize.php create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/cardinal_jwt.php create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/AuthorizeCommandTest.php create mode 100644 dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/SaleCommandTest.php diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php new file mode 100644 index 0000000000000..794c120f94451 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Stub data builder. + * + * Since the order of params is matters for Authorize.net request, + * this builder is used to reserve a place in builders sequence. + */ +class StubDataBuilder implements BuilderInterface +{ + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + return []; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php index ebb95263f54d2..bd439a336786b 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php @@ -17,6 +17,9 @@ use Magento\Quote\Api\Data\PaymentInterface; use PHPUnit\Framework\TestCase; +/** + * Tests DataAssignObserver + */ class DataAssignObserverTest extends TestCase { public function testExecuteSetsProperData() @@ -30,9 +33,7 @@ public function testExecuteSetsProperData() $observerContainer = $this->createMock(Observer::class); $event = $this->createMock(Event::class); $paymentInfoModel = $this->createMock(InfoInterface::class); - $dataObject = new DataObject([ - PaymentInterface::KEY_ADDITIONAL_DATA => $additionalInfo - ]); + $dataObject = new DataObject([PaymentInterface::KEY_ADDITIONAL_DATA => $additionalInfo]); $observerContainer->method('getEvent') ->willReturn($event); $event->method('getDataByKey') diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml index 320f8f79ee28a..730094b8d5524 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml @@ -11,4 +11,24 @@ <argument name="config" xsi:type="object">Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider</argument> </arguments> </type> + <virtualType name="AuthorizenetAcceptjsAuthorizeRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsTransactionRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="transaction_type" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthorizeDataBuilder</item> + <item name="amount" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder</item> + <item name="payment" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PaymentDataBuilder</item> + <item name="shipping" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder</item> + <item name="solution" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\SolutionDataBuilder</item> + <item name="order" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder</item> + <item name="po" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder</item> + <item name="customer" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder</item> + <item name="address" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder</item> + <item name="custom_settings" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder</item> + <item name="passthrough_data" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder</item> + </argument> + </arguments> + </virtualType> </config> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml index cf10557d3869a..02dffe215fcc5 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml @@ -258,6 +258,7 @@ <item name="po" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder</item> <item name="customer" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder</item> <item name="address" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder</item> + <item name="3d_secure" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StubDataBuilder</item> <item name="custom_settings" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder</item> <item name="passthrough_data" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder</item> </argument> diff --git a/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php new file mode 100644 index 0000000000000..3ff4f7dcea065 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetCardinal\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetCardinal\Model\Config; +use Magento\CardinalCommerce\Model\Response\JwtParser; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the cardholder authentication information to the request + */ +class Authorize3DSecureBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var Config + */ + private $config; + + /** + * @var JwtParser + */ + private $jwtParser; + + /** + * @param SubjectReader $subjectReader + * @param Config $config + * @param JwtParser $jwtParser + */ + public function __construct( + SubjectReader $subjectReader, + Config $config, + JwtParser $jwtParser + ) { + $this->subjectReader = $subjectReader; + $this->config = $config; + $this->jwtParser = $jwtParser; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + if ($this->config->isActive() === false) { + return []; + } + + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $cardinalJwt = (string)$payment->getAdditionalInformation('cardinalJWT'); + $jwtPayload = $this->jwtParser->execute($cardinalJwt); + $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag']; + $cavv = $jwtPayload['Payload']['Payment']['ExtendedData']['CAVV']; + $data = [ + 'transactionRequest' => [ + 'cardholderAuthentication' => [ + 'authenticationIndicator' => $eciFlag, + 'cardholderAuthenticationValue' => $cavv + ], + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetCardinal/LICENSE.txt b/app/code/Magento/AuthorizenetCardinal/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/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/AuthorizenetCardinal/LICENSE_AFL.txt b/app/code/Magento/AuthorizenetCardinal/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/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/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php b/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php new file mode 100644 index 0000000000000..d0cde9c643ebf --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetCardinal\Model\Checkout; + +use Magento\AuthorizenetCardinal\Model\Config; +use Magento\Checkout\Model\ConfigProviderInterface; + +/** + * Configuration provider. + */ +class ConfigProvider implements ConfigProviderInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @param Config $config + */ + public function __construct( + Config $config + ) { + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function getConfig(): array + { + $config['cardinal'] = [ + 'isActiveFor' => [ + 'authorizenet' => $this->config->isActive() + ] + ]; + + return $config; + } +} diff --git a/app/code/Magento/AuthorizenetCardinal/Model/Config.php b/app/code/Magento/AuthorizenetCardinal/Model/Config.php new file mode 100644 index 0000000000000..a4390bb10369b --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/Model/Config.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AuthorizenetCardinal\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * AuthorizenetCardinal integration configuration. + * + * Class is a proxy service for retrieving configuration settings. + */ +class Config +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * If this config option set to false no AuthorizenetCardinal integration should be available + * + * @param int|null $storeId + * @return bool + */ + public function isActive(?int $storeId = null): bool + { + $enabled = $this->scopeConfig->isSetFlag( + 'three_d_secure/cardinal/enabled_authorizenet', + ScopeInterface::SCOPE_STORE, + $storeId + ); + + return $enabled; + } +} diff --git a/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php b/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php new file mode 100644 index 0000000000000..cb2cdf64ae389 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetCardinal\Observer; + +use Magento\Framework\Event\Observer; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\AuthorizenetCardinal\Model\Config; + +/** + * Adds the payment info to the payment object + */ +class DataAssignObserver extends AbstractDataAssignObserver +{ + /** + * JWT key + */ + private const JWT_KEY = 'cardinalJWT'; + + /** + * @var Config + */ + private $config; + + /** + * @param Config $config + */ + public function __construct( + Config $config + ) { + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + if ($this->config->isActive() === false) { + return; + } + + $data = $this->readDataArgument($observer); + $additionalData = $data->getData(PaymentInterface::KEY_ADDITIONAL_DATA); + if (!is_array($additionalData)) { + return; + } + + $paymentInfo = $this->readPaymentModelArgument($observer); + if (isset($additionalData[self::JWT_KEY])) { + $paymentInfo->setAdditionalInformation( + self::JWT_KEY, + $additionalData[self::JWT_KEY] + ); + } + } +} diff --git a/app/code/Magento/AuthorizenetCardinal/README.md b/app/code/Magento/AuthorizenetCardinal/README.md new file mode 100644 index 0000000000000..2324f680bafc9 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/README.md @@ -0,0 +1 @@ +The AuthorizenetCardinal module provides a possibility to enable 3-D Secure 2.0 support for AuthorizenetAcceptjs payment integration. \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php b/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php new file mode 100644 index 0000000000000..a45ddc6cfbb30 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetCardinal\Test\Unit\Observer; + +use Magento\AuthorizenetCardinal\Model\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\AuthorizenetCardinal\Observer\DataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; +use PHPUnit\Framework\TestCase; + +/** + * Class DataAssignObserverTest + */ +class DataAssignObserverTest extends TestCase +{ + /** + * Tests setting JWT in payment additional information. + */ + public function testExecuteSetsProperData() + { + $additionalInfo = [ + 'cardinalJWT' => 'foo' + ]; + + $config = $this->createMock(Config::class); + $config->method('isActive') + ->willReturn(true); + $observerContainer = $this->createMock(Observer::class); + $event = $this->createMock(Event::class); + $paymentInfoModel = $this->createMock(InfoInterface::class); + $dataObject = new DataObject([PaymentInterface::KEY_ADDITIONAL_DATA => $additionalInfo]); + $observerContainer->method('getEvent') + ->willReturn($event); + $event->method('getDataByKey') + ->willReturnMap( + [ + [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], + [AbstractDataAssignObserver::DATA_CODE, $dataObject] + ] + ); + $paymentInfoModel->expects($this->once()) + ->method('setAdditionalInformation') + ->with('cardinalJWT', 'foo'); + + $observer = new DataAssignObserver($config); + $observer->execute($observerContainer); + } + + /** + * Tests case when Cardinal JWT is absent. + */ + public function testDoesntSetDataWhenEmpty() + { + $config = $this->createMock(Config::class); + $config->method('isActive') + ->willReturn(true); + $observerContainer = $this->createMock(Observer::class); + $event = $this->createMock(Event::class); + $paymentInfoModel = $this->createMock(InfoInterface::class); + $observerContainer->method('getEvent') + ->willReturn($event); + $event->method('getDataByKey') + ->willReturnMap( + [ + [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], + [AbstractDataAssignObserver::DATA_CODE, new DataObject()] + ] + ); + $paymentInfoModel->expects($this->never()) + ->method('setAdditionalInformation'); + + $observer = new DataAssignObserver($config); + $observer->execute($observerContainer); + } + + /** + * Tests case when CardinalCommerce is disabled. + */ + public function testDoesnttDataWhenEmpty() + { + $config = $this->createMock(Config::class); + $config->method('isActive') + ->willReturn(false); + $observerContainer = $this->createMock(Observer::class); + $observerContainer->expects($this->never()) + ->method('getEvent'); + $observer = new DataAssignObserver($config); + $observer->execute($observerContainer); + } +} diff --git a/app/code/Magento/AuthorizenetCardinal/composer.json b/app/code/Magento/AuthorizenetCardinal/composer.json new file mode 100644 index 0000000000000..e98e41551bcd3 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-authorizenet-cardinal", + "description": "Provides a possibility to enable 3-D Secure 2.0 support for Authorize.Net Acceptjs.", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/module-authorizenet-acceptjs": "*", + "magento/module-cardinal-commerce": "*", + "magento/module-payment": "*", + "magento/module-sales": "*", + "magento/module-quote": "*", + "magento/module-checkout": "*", + "magento/module-store": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AuthorizenetCardinal\\": "" + } + } +} diff --git a/app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..2be287a5e8743 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml @@ -0,0 +1,22 @@ +<?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="three_d_secure"> + <group id="cardinal"> + <group id="config"> + <field id="enabled_authorize" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Enable for Authorize.Net</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>three_d_secure/cardinal/enabled_authorizenet</config_path> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/AuthorizenetCardinal/etc/config.xml b/app/code/Magento/AuthorizenetCardinal/etc/config.xml new file mode 100644 index 0000000000000..d94bcdc479008 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/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> + <three_d_secure> + <cardinal> + <enabled_authorizenet>0</enabled_authorizenet> + </cardinal> + </three_d_secure> + </default> +</config> diff --git a/app/code/Magento/AuthorizenetCardinal/etc/di.xml b/app/code/Magento/AuthorizenetCardinal/etc/di.xml new file mode 100644 index 0000000000000..45541a3cf499a --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/etc/di.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:framework:ObjectManager/etc/config.xsd"> + <virtualType name="AuthorizenetAcceptjsAuthorizeRequest"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="3d_secure" xsi:type="string">Magento\AuthorizenetCardinal\Gateway\Request\Authorize3DSecureBuilder</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/AuthorizenetCardinal/etc/events.xml b/app/code/Magento/AuthorizenetCardinal/etc/events.xml new file mode 100644 index 0000000000000..5b0afbe684699 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/etc/events.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:framework:Event/etc/events.xsd"> + <event name="payment_method_assign_data_authorizenet_acceptjs"> + <observer name="authorizenet_cardinal_data_assign" instance="Magento\AuthorizenetCardinal\Observer\DataAssignObserver" /> + </event> +</config> diff --git a/app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml b/app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml new file mode 100644 index 0000000000000..13c7a223e82d9 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="authorizenet_cardinal_config_provider" xsi:type="object"> + Magento\AuthorizenetCardinal\Model\Checkout\ConfigProvider + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AuthorizenetCardinal/etc/module.xml b/app/code/Magento/AuthorizenetCardinal/etc/module.xml new file mode 100644 index 0000000000000..fdf8151311f43 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/etc/module.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:Module/etc/module.xsd"> + <module name="Magento_AuthorizenetCardinal" > + <sequence> + <module name="Magento_AuthorizenetAcceptjs"/> + <module name="Magento_CardinalCommerce"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/AuthorizenetCardinal/registration.php b/app/code/Magento/AuthorizenetCardinal/registration.php new file mode 100644 index 0000000000000..0153e9eaa4d29 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AuthorizenetCardinal', __DIR__); diff --git a/app/code/Magento/AuthorizenetCardinal/view/frontend/requirejs-config.js b/app/code/Magento/AuthorizenetCardinal/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..81823cb2afc58 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/view/frontend/requirejs-config.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_AuthorizenetAcceptjs/js/view/payment/method-renderer/authorizenet-accept': { + 'Magento_AuthorizenetCardinal/js/authorizenet-accept-mixin': true + } + } + } +}; + diff --git a/app/code/Magento/AuthorizenetCardinal/view/frontend/web/js/authorizenet-accept-mixin.js b/app/code/Magento/AuthorizenetCardinal/view/frontend/web/js/authorizenet-accept-mixin.js new file mode 100644 index 0000000000000..336ceaab3ec67 --- /dev/null +++ b/app/code/Magento/AuthorizenetCardinal/view/frontend/web/js/authorizenet-accept-mixin.js @@ -0,0 +1,70 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_CardinalCommerce/js/cardinal-client', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Ui/js/model/messageList' +], function ($, cardinalClient, fullScreenLoader, globalMessageList) { + 'use strict'; + + return function (originalComponent) { + return originalComponent.extend({ + defaults: { + cardinalJWT: null + }, + + /** + * Performs 3d-secure authentication + */ + beforePlaceOrder: function () { + var original = this._super.bind(this), + client = cardinalClient, + isActive = window.checkoutConfig.cardinal.isActiveFor.authorizenet, + cardData; + + if (!isActive || !$(this.formElement).valid()) { + return original(); + } + + cardData = { + accountNumber: this.creditCardNumber(), + expMonth: this.creditCardExpMonth(), + expYear: this.creditCardExpYear() + }; + + if (this.hasVerification()) { + cardData.cardCode = this.creditCardVerificationNumber(); + } + + fullScreenLoader.startLoader(); + client.startAuthentication(cardData) + .always(function () { + fullScreenLoader.stopLoader(); + }) + .done(function (jwt) { + this.cardinalJWT = jwt; + original(); + }.bind(this)) + .fail(function (errorMessage) { + globalMessageList.addErrorMessage({ + message: errorMessage + }); + }); + }, + + /** + * @returns {Object} + */ + getData: function () { + var originalData = this._super(); + + originalData['additional_data'].cardinalJWT = this.cardinalJWT; + + return originalData; + } + }); + }; +}); diff --git a/app/code/Magento/CardinalCommerce/LICENSE.txt b/app/code/Magento/CardinalCommerce/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CardinalCommerce/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/CardinalCommerce/LICENSE_AFL.txt b/app/code/Magento/CardinalCommerce/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/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/CardinalCommerce/Model/Adminhtml/Source/Environment.php b/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php new file mode 100644 index 0000000000000..29e2939a0ae50 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Adminhtml\Source; + +/** + * CardinalCommerce Environment Dropdown source + */ +class Environment implements \Magento\Framework\Data\OptionSourceInterface +{ + private const ENVIRONMENT_PRODUCTION = 'production'; + private const ENVIRONMENT_SANDBOX = 'sandbox'; + + /** + * Possible environment types + * + * @return array + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => self::ENVIRONMENT_SANDBOX, + 'label' => 'Sandbox', + ], + [ + 'value' => self::ENVIRONMENT_PRODUCTION, + 'label' => 'Production' + ] + ]; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php b/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php new file mode 100644 index 0000000000000..a6794eae90084 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Checkout; + +use Magento\CardinalCommerce\Model\Config; +use Magento\CardinalCommerce\Model\Request\TokenBuilder; +use Magento\Checkout\Model\ConfigProviderInterface; + +/** + * Configuration provider. + */ +class ConfigProvider implements ConfigProviderInterface +{ + /** + * @var TokenBuilder + */ + private $requestJwtBuilder; + + /** + * @var Config + */ + private $config; + + /** + * @param TokenBuilder $requestJwtBuilder + * @param Config $config + */ + public function __construct( + TokenBuilder $requestJwtBuilder, + Config $config + ) { + $this->requestJwtBuilder = $requestJwtBuilder; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function getConfig(): array + { + $config['cardinal'] = [ + 'environment' => $this->config->getEnvironment(), + 'requestJWT' => $this->requestJwtBuilder->build() + ]; + + return $config; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Config.php b/app/code/Magento/CardinalCommerce/Model/Config.php new file mode 100644 index 0000000000000..0975ed77c9708 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Config.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CardinalCommerce\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * CardinalCommerce integration configuration. + * + * Class is a proxy service for retrieving configuration settings. + */ +class Config +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Returns CardinalCommerce API Key used for authentication. + * + * A shared secret value between the merchant and Cardinal. This value should never be exposed to the public. + * + * @param int|null $storeId + * @return string + */ + public function getApiKey(?int $storeId = null): string + { + $apiKey = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/api_key', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $apiKey; + } + + /** + * Returns CardinalCommerce API Identifier. + * + * GUID used to identify the specific API Key. + * + * @param int|null $storeId + * @return string + */ + public function getApiIdentifier(?int $storeId = null): string + { + $apiIdentifier = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/api_identifier', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $apiIdentifier; + } + + /** + * Returns CardinalCommerce Org Unit Id. + * + * GUID to identify the merchant organization within Cardinal systems. + * + * @param int|null $storeId + * @return string + */ + public function getOrgUnitId(?int $storeId = null): string + { + $orgUnitId = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/org_unit_id', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $orgUnitId; + } + + /** + * Returns CardinalCommerce environment. + * + * Sandbox or production. + * + * @param int|null $storeId + * @return string + */ + public function getEnvironment(?int $storeId = null): string + { + $orgUnitId = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/environment', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $orgUnitId; + } + + /** + * If is "true" extra information about interaction with CardinalCommerce API are written to payment.log file + * + * @param int|null $storeId + * @return bool + */ + public function isDebugModeEnabled(?int $storeId = null): bool + { + $debugModeEnabled = $this->scopeConfig->isSetFlag( + 'three_d_secure/cardinal/debug', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $debugModeEnabled; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php new file mode 100644 index 0000000000000..cfbdb2d8a1f1b --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model; + +use Magento\Framework\Serialize\Serializer\Json; + +/** + * JSON Web Token management. + */ +class JwtManagement +{ + /** + * The signing algorithm. Cardinal supported algorithm is 'HS256' + */ + private const SIGN_ALGORITHM = 'HS256'; + + /** + * @var Json + */ + private $json; + + /** + * @param Json $json + */ + public function __construct( + Json $json + ) { + $this->json = $json; + } + + /** + * Converts JWT string into array. + * + * @param string $jwt The JWT + * @param string $key The secret key + * + * @return array + * @throws \InvalidArgumentException + */ + public function decode(string $jwt, string $key): array + { + if (empty($jwt)) { + throw new \InvalidArgumentException('JWT is empty'); + } + + $parts = explode('.', $jwt); + if (count($parts) != 3) { + throw new \InvalidArgumentException('Wrong number of segments in JWT'); + } + + list($headB64, $payloadB64, $signatureB64) = $parts; + + $headerJson = $this->urlSafeB64Decode($headB64); + $header = $this->json->unserialize($headerJson); + + $payloadJson = $this->urlSafeB64Decode($payloadB64); + $payload = $this->json->unserialize($payloadJson); + + $signature = $this->urlSafeB64Decode($signatureB64); + if ($signature !== $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg'])) { + throw new \InvalidArgumentException('JWT signature verification failed'); + } + + return $payload; + } + + /** + * Converts and signs array into a JWT string. + * + * @param array $payload + * @param string $key + * + * @return string + * @throws \InvalidArgumentException + */ + public function encode(array $payload, string $key): string + { + $header = ['typ' => 'JWT', 'alg' => self::SIGN_ALGORITHM]; + + $headerJson = $this->json->serialize($header); + $segments[] = $this->urlSafeB64Encode($headerJson); + + $payloadJson = $this->json->serialize($payload); + $segments[] = $this->urlSafeB64Encode($payloadJson); + + $signature = $this->sign(implode('.', $segments), $key, $header['alg']); + $segments[] = $this->urlSafeB64Encode($signature); + + return implode('.', $segments); + } + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign. + * @param string $key The secret key. + * @param string $algorithm The signing algorithm. + * + * @return string + * @throws \InvalidArgumentException + */ + private function sign(string $msg, string $key, string $algorithm): string + { + if ($algorithm !== self::SIGN_ALGORITHM) { + throw new \InvalidArgumentException('Algorithm ' . $algorithm . ' is not supported'); + } + + return hash_hmac('sha256', $msg, $key, true); + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string + */ + private function urlSafeB64Decode(string $input): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_decode( + str_pad(strtr($input, '-_', '+/'), strlen($input) % 4, '=', STR_PAD_RIGHT) + ); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string + */ + private function urlSafeB64Encode(string $input): string + { + return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php b/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php new file mode 100644 index 0000000000000..e045d00dc55fe --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Request; + +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\CardinalCommerce\Model\Config; +use Magento\Checkout\Model\Session; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Intl\DateTimeFactory; + +/** + * Cardinal request token builder. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class TokenBuilder +{ + /** + * @var JwtManagement + */ + private $jwtManagement; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @var Config + */ + private $config; + + /** + * @var IdentityGeneratorInterface + */ + private $identityGenerator; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param JwtManagement $jwtManagement + * @param Session $checkoutSession + * @param Config $config + * @param IdentityGeneratorInterface $identityGenerator + * @param DateTimeFactory $dateTimeFactory + */ + public function __construct( + JwtManagement $jwtManagement, + Session $checkoutSession, + Config $config, + IdentityGeneratorInterface $identityGenerator, + DateTimeFactory $dateTimeFactory + ) { + $this->jwtManagement = $jwtManagement; + $this->checkoutSession = $checkoutSession; + $this->config = $config; + $this->identityGenerator = $identityGenerator; + $this->dateTimeFactory = $dateTimeFactory; + } + + /** + * Builds request JWT. + * + * @return string + */ + public function build() + { + $quote = $this->checkoutSession->getQuote(); + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $orderDetails = [ + 'OrderDetails' => [ + 'OrderNumber' => $quote->getId(), + 'Amount' => $quote->getBaseGrandTotal() * 100, + 'CurrencyCode' => $quote->getBaseCurrencyCode() + ] + ]; + + $token = [ + 'jti' => $this->identityGenerator->generateId(), + 'iss' => $this->config->getApiIdentifier(), + 'iat' => $currentDate->getTimestamp(), + 'OrgUnitId' => $this->config->getOrgUnitId(), + 'Payload' => $orderDetails, + 'ObjectifyPayload' => true + ]; + + $jwt = $this->jwtManagement->encode($token, $this->config->getApiKey()); + + return $jwt; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php new file mode 100644 index 0000000000000..1865605d50acc --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Response; + +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\CardinalCommerce\Model\Config; +use Magento\Framework\Exception\LocalizedException; +use Psr\Log\LoggerInterface; +use Magento\Payment\Model\Method\Logger as PaymentLogger; + +/** + * Parse content of CardinalCommerce response JWT. + */ +class JwtParser +{ + /** + * @var JwtManagement + */ + private $jwtManagement; + + /** + * @var Config + */ + private $config; + + /** + * @var JwtPayloadValidatorInterface + */ + private $tokenValidator; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var PaymentLogger + */ + private $paymentLogger; + + /** + * @param JwtManagement $jwtManagement + * @param Config $config + * @param JwtPayloadValidatorInterface $tokenValidator + * @param PaymentLogger $paymentLogger + * @param LoggerInterface $logger + */ + public function __construct( + JwtManagement $jwtManagement, + Config $config, + JwtPayloadValidatorInterface $tokenValidator, + PaymentLogger $paymentLogger, + LoggerInterface $logger + ) { + $this->jwtManagement = $jwtManagement; + $this->config = $config; + $this->tokenValidator = $tokenValidator; + $this->paymentLogger = $paymentLogger; + $this->logger = $logger; + } + + /** + * Returns response JWT payload. + * + * @param string $jwt + * @return array + * @throws LocalizedException + */ + public function execute(string $jwt): array + { + $jwtPayload = ''; + try { + $this->debug(['Cardinal Response JWT:' => $jwt]); + $jwtPayload = $this->jwtManagement->decode($jwt, $this->config->getApiKey()); + $this->debug(['Cardinal Response JWT payload:' => $jwtPayload]); + if (!$this->tokenValidator->validate($jwtPayload)) { + $this->throwException(); + } + } catch (\InvalidArgumentException $e) { + $this->logger->critical($e, ['CardinalCommerce3DSecure']); + $this->throwException(); + } + + return $jwtPayload; + } + + /** + * Log JWT data. + * + * @param array $data + * @return void + */ + private function debug(array $data) + { + if ($this->config->isDebugModeEnabled()) { + $this->paymentLogger->debug($data, ['iss'], true); + } + } + + /** + * Throw general localized exception. + * + * @return void + * @throws LocalizedException + */ + private function throwException() + { + throw new LocalizedException( + __( + 'Authentication Failed. Your card issuer cannot authenticate this card. ' . + 'Please select another card or form of payment to complete your purchase.' + ) + ); + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php new file mode 100644 index 0000000000000..9720b90cad915 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Response; + +use Magento\Framework\Intl\DateTimeFactory; + +/** + * Validates payload of CardinalCommerce response JWT. + */ +class JwtPayloadValidator implements JwtPayloadValidatorInterface +{ + /** + * Resulting state of the transaction. + * + * SUCCESS - The transaction resulted in success for the payment type used. For example, + * with a CCA transaction this would indicate the user has successfully completed authentication. + * + * NOACTION - The transaction was successful but requires in no additional action. For example, + * with a CCA transaction this would indicate that the user is not currently enrolled in 3D Secure, + * but the API calls were successful. + * + * FAILURE - The transaction resulted in an error. For example, with a CCA transaction this would indicate + * that the user failed authentication or an error was encountered while processing the transaction. + * + * ERROR - A service level error was encountered. These are generally reserved for connectivity + * or API authentication issues. For example if your JWT was incorrectly signed, or Cardinal + * services are currently unreachable. + * + * @var array + */ + private $allowedActionCode = ['SUCCESS', 'NOACTION']; + + /** + * 3DS status of transaction from ECI Flag value. Liability shift applies. + * + * 05 - Successful 3D Authentication (Visa, AMEX, JCB) + * 02 - Successful 3D Authentication (MasterCard) + * 06 - Attempted Processing or User Not Enrolled (Visa, AMEX, JCB) + * 01 - Attempted Processing or User Not Enrolled (MasterCard) + * 07 - 3DS authentication is either failed or could not be attempted; + * possible reasons being both card and Issuing Bank are not secured by 3DS, + * technical errors, or improper configuration. (Visa, AMEX, JCB) + * 00 - 3DS authentication is either failed or could not be attempted; + * possible reasons being both card and Issuing Bank are not secured by 3DS, + * technical errors, or improper configuration. (MasterCard) + * + * @var array + */ + private $allowedECIFlag = ['05', '02', '06', '01']; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param DateTimeFactory $dateTimeFactory + */ + public function __construct( + DateTimeFactory $dateTimeFactory + ) { + $this->dateTimeFactory = $dateTimeFactory; + } + /** + * @inheritdoc + */ + public function validate(array $jwtPayload): bool + { + $transactionState = $jwtPayload['Payload']['ActionCode'] ?? ''; + $errorNumber = $jwtPayload['Payload']['ErrorNumber'] ?? -1; + $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag'] ?? ''; + $expTimestamp = $jwtPayload['exp'] ?? 0; + + return $this->isValidErrorNumber((int)$errorNumber) + && $this->isValidTransactionState($transactionState) + && $this->isValidEciFlag($eciFlag) + && $this->isNotExpired((int)$expTimestamp); + } + + /** + * Checks application error number. + * + * A non-zero value represents the error encountered while attempting the process the message request. + * + * @param int $errorNumber + * @return bool + */ + private function isValidErrorNumber(int $errorNumber) + { + return $errorNumber === 0; + } + + /** + * Checks if value of transaction state identifier is in allowed list. + * + * @param string $transactionState + * @return bool + */ + private function isValidTransactionState(string $transactionState) + { + return in_array($transactionState, $this->allowedActionCode); + } + + /** + * Checks if value of ECI Flag identifier is in allowed list. + * + * @param string $eciFlag + * @return bool + */ + private function isValidEciFlag(string $eciFlag) + { + return in_array($eciFlag, $this->allowedECIFlag); + } + + /** + * Checks if token is not expired. + * + * @param int $expTimestamp + * @return bool + */ + private function isNotExpired(int $expTimestamp) + { + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + + return $currentDate->getTimestamp() < $expTimestamp; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php new file mode 100644 index 0000000000000..774c0daee6ca2 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CardinalCommerce\Model\Response; + +/** + * Validates payload of CardinalCommerce response JWT. + */ +interface JwtPayloadValidatorInterface +{ + /** + * Validates token payload. + * + * @param array $jwtPayload + * @return bool + */ + public function validate(array $jwtPayload); +} diff --git a/app/code/Magento/CardinalCommerce/README.md b/app/code/Magento/CardinalCommerce/README.md new file mode 100644 index 0000000000000..54db9114a2a0e --- /dev/null +++ b/app/code/Magento/CardinalCommerce/README.md @@ -0,0 +1 @@ +The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. \ No newline at end of file diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/JwtManagementTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/JwtManagementTest.php new file mode 100644 index 0000000000000..70eae201c157a --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Unit/JwtManagementTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Test\Unit\Model; + +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Tests JWT encode and decode. + */ +class JwtManagementTest extends \PHPUnit\Framework\TestCase +{ + /** + * API key + */ + private const API_KEY = 'API key'; + + /** + * @var JwtManagement + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->model = new JwtManagement(new Json()); + } + + /** + * Tests JWT encode. + */ + public function testEncode() + { + $jwt = $this->model->encode($this->getTokenPayload(), self::API_KEY); + + $this->assertEquals( + $this->getValidHS256Jwt(), + $jwt, + 'Generated JWT isn\'t equal to expected' + ); + } + + /** + * Tests JWT decode. + */ + public function testDecode() + { + $tokenPayload = $this->model->decode($this->getValidHS256Jwt(), self::API_KEY); + + $this->assertEquals( + $this->getTokenPayload(), + $tokenPayload, + 'JWT payload isn\'t equal to expected' + ); + } + + /** + * Tests JWT decode. + * + * @param string $jwt + * @param string $errorMessage + * @dataProvider decodeWithExceptionDataProvider + */ + public function testDecodeWithException(string $jwt, string $errorMessage) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($errorMessage); + + $this->model->decode($jwt, self::API_KEY); + } + + /** + * @return array + */ + public function decodeWithExceptionDataProvider(): array + { + return [ + [ + 'jwt' => '', + 'errorMessage' => 'JWT is empty', + ], + [ + 'jwt' => 'dddd.dddd', + 'errorMessage' => 'Wrong number of segments in JWT', + ], + [ + 'jwt' => 'dddd.dddd.dddd', + 'errorMessage' => 'Unable to unserialize value. Error: Syntax error', + ], + [ + 'jwt' => $this->getHS512Jwt(), + 'errorMessage' => 'Algorithm HS512 is not supported', + ], + [ + 'jwt' => $this->getJwtWithInvalidSignature(), + 'errorMessage' => 'JWT signature verification failed', + ], + ]; + } + + /** + * Returns valid JWT, signed using HS256. + * + * @return string + */ + private function getValidHS256Jwt(): string + { + return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . + 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1Vua' . + 'XRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGVyTnV' . + 'tYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF5bG9hZ' . + 'CI6dHJ1ZX0.emv9N75JIvyk_gQHMNJYQ2UzmbM5ISBQs1Y222zO1jk'; + } + + /** + * Returns JWT, signed using not supported HS512. + * + * @return string + */ + private function getHS512Jwt(): string + { + return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . + 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1V' . + 'uaXRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGV' . + 'yTnVtYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF' . + '5bG9hZCI6dHJ1ZX0.4fwdXfIgUe7bAiHP2bZsxSG-s-wJOyaCmCe9MOQhs-m6LLjRGarguA_0SqZA2EeUaytMO4R' . + 'G84ZEOfbYfS8c1A'; + } + + /** + * Returns JWT with invalid signature. + * + * @return string + */ + private function getJwtWithInvalidSignature(): string + { + return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . + 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1Vua' . + 'XRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGVyTnV' . + 'tYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF5bG9hZ' . + 'CI6dHJ1ZX0.InvalidSign'; + } + + /** + * Returns token decoded payload. + * + * @return array + */ + private function getTokenPayload(): array + { + return [ + 'jti' => 'a5a59bfb-ac06-4c5f-be5c-351b64ae608e', + 'iss' => '56560a358b946e0c8452365ds', + 'iat' => '1448997865', + 'OrgUnitId' => '565607c18b946e058463ds8r', + 'Payload' => [ + 'OrderDetails' => [ + 'OrderNumber' => '125', + 'Amount' => '1500', + 'CurrencyCode' => 'USD' + ] + ], + 'ObjectifyPayload' => true + ]; + } +} diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php new file mode 100644 index 0000000000000..cbaae9f777a61 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Test\Unit\Model\Response; + +use Magento\CardinalCommerce\Model\Response\JwtPayloadValidator; +use Magento\Framework\Intl\DateTimeFactory; + +/** + * Class JwtPayloadValidatorTest + */ +class JwtPayloadValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var JwtPayloadValidator + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->model = new JwtPayloadValidator(new DateTimeFactory()); + } + + /** + * Tests successful cases. + * + * @param array $token + * @dataProvider validateSuccessDataProvider + */ + public function testValidateSuccess(array $token) + { + $this->assertTrue( + $this->model->validate($token) + ); + } + + /** + * @case 1. All data are correct, the transaction was successful (Visa, AMEX) + * @case 2. All data are correct, the transaction was successful but requires in no additional action (Visa, AMEX) + * @case 3. All data are correct, the transaction was successful (MasterCard) + * @case 4. All data are correct, the transaction was successful but requires in no additional action (MasterCard) + * + * @return array + */ + public function validateSuccessDataProvider() + { + $expTimestamp = $this->getValidExpTimestamp(); + + return [ + 1 => $this->createToken('05', '0', 'SUCCESS', $expTimestamp), + 2 => $this->createToken('06', '0', 'NOACTION', $expTimestamp), + 3 => $this->createToken('02', '0', 'SUCCESS', $expTimestamp), + 4 => $this->createToken('01', '0', 'NOACTION', $expTimestamp), + ]; + } + + /** + * Case when 3DS authentication is either failed or could not be attempted. + * + * @param array $token + * @dataProvider validationEciFailsDataProvider + */ + public function testValidationEciFails(array $token) + { + $this->assertFalse( + $this->model->validate($token), + 'Negative ECIFlag value validation fails' + ); + } + + /** + * ECIFlag value when 3DS authentication is either failed or could not be attempted. + * + * @case 1. Visa, AMEX, JCB + * @case 2. MasterCard + * @return array + */ + public function validationEciFailsDataProvider(): array + { + $expTimestamp = $this->getValidExpTimestamp(); + return [ + 1 => $this->createToken('07', '0', 'NOACTION', $expTimestamp), + 2 => $this->createToken('00', '0', 'NOACTION', $expTimestamp), + ]; + } + + /** + * Case when resulting state of the transaction is negative. + * + * @param array $token + * @dataProvider validationActionCodeFailsDataProvider + */ + public function testValidationActionCodeFails(array $token) + { + $this->assertFalse( + $this->model->validate($token), + 'Negative ActionCode value validation fails' + ); + } + + /** + * ECIFlag value when 3DS authentication is either failed or could not be attempted. + * + * @return array + */ + public function validationActionCodeFailsDataProvider(): array + { + $expTimestamp = $this->getValidExpTimestamp(); + return [ + $this->createToken('05', '0', 'FAILURE', $expTimestamp), + $this->createToken('05', '0', 'ERROR', $expTimestamp), + ]; + } + + /** + * Case when ErrorNumber not equal 0. + */ + public function testValidationErrorNumberFails() + { + $notAllowedErrorNumber = '10'; + $expTimestamp = $this->getValidExpTimestamp(); + $token = $this->createToken('05', $notAllowedErrorNumber, 'SUCCESS', $expTimestamp); + $this->assertFalse( + $this->model->validate($token), + 'Negative ErrorNumber value validation fails' + ); + } + + /** + * Case when ErrorNumber not equal 0. + */ + public function testValidationExpirationFails() + { + $expTimestamp = $this->getOutdatedExpTimestamp(); + $token = $this->createToken('05', '0', 'SUCCESS', $expTimestamp); + $this->assertFalse( + $this->model->validate($token), + 'Expiration date validation fails' + ); + } + + /** + * Creates a token. + * + * @param string $eciFlag + * @param string $errorNumber + * @param string $actionCode + * @param int $expTimestamp + * + * @return array + */ + private function createToken(string $eciFlag, string $errorNumber, string $actionCode, int $expTimestamp): array + { + return [ + [ + 'Payload' => [ + 'Payment' => [ + 'ExtendedData' => [ + 'ECIFlag' => $eciFlag, + ], + ], + 'ActionCode' => $actionCode, + 'ErrorNumber' => $errorNumber + ], + 'exp' => $expTimestamp + ] + ]; + } + + /** + * Returns valid expiration timestamp. + * + * @return int + */ + private function getValidExpTimestamp() + { + $dateTimeFactory = new DateTimeFactory(); + $currentDate = $dateTimeFactory->create('now', new \DateTimeZone('UTC')); + + return $currentDate->getTimestamp() + 3600; + } + + /** + * Returns outdated expiration timestamp. + * + * @return int + */ + private function getOutdatedExpTimestamp() + { + $dateTimeFactory = new DateTimeFactory(); + $currentDate = $dateTimeFactory->create('now', new \DateTimeZone('UTC')); + + return $currentDate->getTimestamp() - 3600; + } +} diff --git a/app/code/Magento/CardinalCommerce/composer.json b/app/code/Magento/CardinalCommerce/composer.json new file mode 100644 index 0000000000000..3e839228dc79a --- /dev/null +++ b/app/code/Magento/CardinalCommerce/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-cardinal-commerce", + "description": "Provides a possibility to enable 3-D Secure 2.0 support for payment methods.", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-payment": "*", + "magento/module-store": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CardinalCommerce\\": "" + } + } +} diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..ef44f31e08e69 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml @@ -0,0 +1,48 @@ +<?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="three_d_secure" translate="label" type="text" sortOrder="410" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>3D Secure</label> + <tab>sales</tab> + <resource>Magento_Sales::three_d_secure</resource> + <group id="cardinal" type="text" sortOrder="13" showInDefault="1" showInWebsite="1" showInStore="0"> + <group id="config" translate="label comment" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Configuration</label> + <comment><![CDATA[For support contact <a href="mailto:support@cardinalcommerce.com">support@cardinalcommerce.com</a>.]]> + </comment> + <field id="environment" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Environment</label> + <source_model>Magento\CardinalCommerce\Model\Adminhtml\Source\Environment</source_model> + <config_path>three_d_secure/cardinal/environment</config_path> + </field> + <field id="org_unit_id" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Org Unit Id</label> + <config_path>three_d_secure/cardinal/org_unit_id</config_path> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> + <field id="api_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>API Key</label> + <config_path>three_d_secure/cardinal/api_key</config_path> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> + <field id="api_identifier" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>API Identifier</label> + <config_path>three_d_secure/cardinal/api_identifier</config_path> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> + <field id="debug" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Debug</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>three_d_secure/cardinal/debug</config_path> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/config.xml b/app/code/Magento/CardinalCommerce/etc/config.xml new file mode 100644 index 0000000000000..60b111a59cbc9 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/config.xml @@ -0,0 +1,20 @@ +<?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> + <three_d_secure> + <cardinal> + <environment>production</environment> + <api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <org_unit_id backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <api_identifier backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <debug>0</debug> + </cardinal> + </three_d_secure> + </default> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/di.xml b/app/code/Magento/CardinalCommerce/etc/di.xml new file mode 100644 index 0000000000000..ffd3c50ef5043 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/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\CardinalCommerce\Model\Response\JwtPayloadValidatorInterface" type="Magento\CardinalCommerce\Model\Response\JwtPayloadValidator" /> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/frontend/di.xml b/app/code/Magento/CardinalCommerce/etc/frontend/di.xml new file mode 100644 index 0000000000000..e3913291aa683 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/frontend/di.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="cardinal_config_provider" xsi:type="object"> + Magento\CardinalCommerce\Model\Checkout\ConfigProvider + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/module.xml b/app/code/Magento/CardinalCommerce/etc/module.xml new file mode 100644 index 0000000000000..8605e81d3fda7 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/module.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:Module/etc/module.xsd"> + <module name="Magento_CardinalCommerce" > + <sequence> + <module name="Magento_Checkout"/> + <module name="Magento_Payment"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CardinalCommerce/registration.php b/app/code/Magento/CardinalCommerce/registration.php new file mode 100644 index 0000000000000..26fb168fb0ae2 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CardinalCommerce', __DIR__); diff --git a/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js b/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..0c5e3964d04e7 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + shim: { + cardinaljs: { + exports: 'Cardinal' + }, + cardinaljsSandbox: { + exports: 'Cardinal' + } + }, + paths: { + cardinaljsSandbox: 'https://includestest.ccdc02.com/cardinalcruise/v1/songbird', + cardinaljs: 'https://songbird.cardinalcommerce.com/edge/v1/songbird' + } +}; + diff --git a/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js new file mode 100644 index 0000000000000..2ddb450d2f81c --- /dev/null +++ b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js @@ -0,0 +1,131 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiClass', + 'Magento_CardinalCommerce/js/cardinal-factory', + 'Magento_Checkout/js/model/quote', + 'mage/translate' +], function ($, Class, cardinalFactory, quote, $t) { + 'use strict'; + + return { + /** + * Starts Cardinal Consumer Authentication + * + * @param {Object} cardData + * @return {jQuery.Deferred} + */ + startAuthentication: function (cardData) { + var deferred = $.Deferred(); + + if (this.cardinalClient) { + this._startAuthentication(deferred, cardData); + } else { + cardinalFactory(this.getEnvironment()) + .done(function (client) { + this.cardinalClient = client; + this._startAuthentication(deferred, cardData); + }.bind(this)); + } + + return deferred.promise(); + }, + + /** + * Cardinal Consumer Authentication + * + * @param {jQuery.Deferred} deferred + * @param {Object} cardData + */ + _startAuthentication: function (deferred, cardData) { + //this.cardinalClient.configure({ logging: { level: 'verbose' } }); + this.cardinalClient.on('payments.validated', function (data, jwt) { + if (data.ErrorNumber !== 0) { + deferred.reject(data.ErrorDescription); + } else if ($.inArray(data.ActionCode, ['FAILURE', 'ERROR']) !== -1) { + deferred.reject($t('Authentication Failed. Please try again with another form of payment.')); + } else { + deferred.resolve(jwt); + } + this.cardinalClient.off('payments.validated'); + }.bind(this)); + + this.cardinalClient.on('payments.setupComplete', function () { + this.cardinalClient.start('cca', this.getRequestOrderObject(cardData)); + this.cardinalClient.off('payments.setupComplete'); + }.bind(this)); + + this.cardinalClient.setup('init', { + jwt: this.getRequestJWT() + }); + }, + + /** + * Returns request order object. + * + * The request order object is structured object that is used to pass data + * to Cardinal that describes an order you'd like to process. + * + * If you pass a request object in both the JWT and the browser, + * Cardinal will merge the objects together where the browser overwrites + * the JWT object as it is considered the most recently captured data. + * + * @param {Object} cardData + * @returns {Object} + */ + getRequestOrderObject: function (cardData) { + var totalAmount = quote.totals()['base_grand_total'], + currencyCode = quote.totals()['base_currency_code'], + billingAddress = quote.billingAddress(), + requestObject; + + requestObject = { + OrderDetails: { + Amount: totalAmount * 100, + CurrencyCode: currencyCode + }, + Consumer: { + Account: { + AccountNumber: cardData.accountNumber, + ExpirationMonth: cardData.expMonth, + ExpirationYear: cardData.expYear, + CardCode: cardData.cardCode + }, + BillingAddress: { + FirstName: billingAddress.firstname, + LastName: billingAddress.lastname, + Address1: billingAddress.street[0], + Address2: billingAddress.street[1], + City: billingAddress.city, + State: billingAddress.region, + PostalCode: billingAddress.postcode, + CountryCode: billingAddress.countryId, + Phone1: billingAddress.telephone + } + } + }; + + return requestObject; + }, + + /** + * Returns request JWT + * @returns {String} + */ + getRequestJWT: function () { + return window.checkoutConfig.cardinal.requestJWT; + }, + + /** + * Returns type of environment + * @returns {String} + */ + getEnvironment: function () { + return window.checkoutConfig.cardinal.environment; + } + }; +}); diff --git a/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js new file mode 100644 index 0000000000000..1da92ba2ff787 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (environment) { + var deferred = $.Deferred(), + dependency = 'cardinaljs'; + + if (environment === 'sandbox') { + dependency = 'cardinaljsSandbox'; + } + + require( + [dependency], + function (Cardinal) { + deferred.resolve(Cardinal); + }, + deferred.reject + ); + + return deferred.promise(); + }; +}); diff --git a/composer.json b/composer.json index 1d3e4cef5c7e6..434ccee36b8ee 100644 --- a/composer.json +++ b/composer.json @@ -106,6 +106,7 @@ "magento/module-authorization": "*", "magento/module-authorizenet": "*", "magento/module-authorizenet-acceptjs": "*", + "magento/module-authorizenet-cardinal": "*", "magento/module-advanced-search": "*", "magento/module-backend": "*", "magento/module-backup": "*", @@ -115,6 +116,7 @@ "magento/module-bundle-import-export": "*", "magento/module-cache-invalidate": "*", "magento/module-captcha": "*", + "magento/module-cardinal-commerce": "*", "magento/module-catalog": "*", "magento/module-catalog-analytics": "*", "magento/module-catalog-import-export": "*", diff --git a/composer.lock b/composer.lock index 4e052c61fd460..00c0d0163bc29 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": "33e7703ac47e1c27235b830825e14800", + "content-hash": "f208adf6f5c6685484ce11c84b942f2f", "packages": [ { "name": "braintree/braintree_php", @@ -6771,7 +6771,7 @@ "time": "2019-04-29T20:56:26+00:00" }, { - "name": "mikey179/vfsStream", + "name": "mikey179/vfsstream", "version": "v1.6.5", "source": { "type": "git", diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/authorize.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/authorize.php new file mode 100644 index 0000000000000..ceab22403d987 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/authorize.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'authOnlyTransaction', + 'amount' => '100.00', + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'mydescriptor', + 'dataValue' => 'myvalue', + ], + ], + 'solution' => [ + 'id' => 'AAA102993', + ], + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => 1, + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1', + 'cardholderAuthentication' => [ + 'authenticationIndicator' => '05', + 'cardholderAuthenticationValue' => 'AAABAWFlmQAAAABjRWWZEEFgFz8=', + ], + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'authOnlyTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/sale.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/sale.php new file mode 100644 index 0000000000000..f96facb19b3b5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/expected_request/sale.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'authCaptureTransaction', + 'amount' => '100.00', + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'mydescriptor', + 'dataValue' => 'myvalue', + ], + ], + 'solution' => [ + 'id' => 'AAA102993', + ], + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => 1, + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1', + 'cardholderAuthentication' => [ + 'authenticationIndicator' => '05', + 'cardholderAuthenticationValue' => 'AAABAWFlmQAAAABjRWWZEEFgFz8=', + ], + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'authCaptureTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/full_order_with_3dsecure.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/full_order_with_3dsecure.php new file mode 100644 index 0000000000000..4f50b502e8554 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/full_order_with_3dsecure.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Sales\Model\Order\Payment; +use Magento\TestFramework\Helper\Bootstrap; + +$order = include __DIR__ . '/../../AuthorizenetAcceptjs/_files/full_order.php'; + +$objectManager = Bootstrap::getObjectManager(); +$cardinalJWT = include __DIR__ . '/response/cardinal_jwt.php'; + +/** @var Payment $payment */ +$payment = $order->getPayment(); +$payment->setAdditionalInformation('cardinalJWT', $cardinalJWT); + +return $order; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/authorize.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/authorize.php new file mode 100644 index 0000000000000..3af04813c1ac8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/authorize.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => 'abc123', + 'avsResultCode' => 'P', + 'cvvResultCode' => '', + 'cavvResultCode' => '', + 'transId' => '123456', + 'refTransID' => '', + 'transHash' => 'foobar', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'userFields' => [ + [ + 'name' => 'transactionType', + 'value' => 'authOnlyTransaction' + ] + ], + 'transHashSha2' => 'CD1E57FB1B5C876FDBD536CB16F8BBBA687580EDD78DD881C7F14DC4467C32BF6C' + . '808620FBD59E5977DF19460B98CCFC0DA0D90755992C0D611CABB8E2BA52B0', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/cardinal_jwt.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/cardinal_jwt.php new file mode 100644 index 0000000000000..80f42524897f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Fixture/response/cardinal_jwt.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\CardinalCommerce\Model\Config; +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var JwtManagement $jwtManagment */ +$jwtManagment = $objectManager->get(JwtManagement::class); +/** @var Config $config */ +$config = $objectManager->get(Config::class); +$currentDate = new \DateTime('now', new \DateTimeZone('UTC')); +$response = [ + 'iss' => 'some_api_identifier', + 'iat' => 1559855656, + 'exp' => $currentDate->getTimestamp() + 3600, + 'jti' => '0d695df5-ca06-4f7d-b150-ff169510f6d2', + 'ConsumerSessionId' => '0_9e6a4084-2191-4fd7-9631-19f576375e0a', + 'ReferenceId' => '0_9e6a4084-2191-4fd7-9631-19f576375e0a', + 'aud' => '52efb9cc-843c-4ee9-a38c-107943be6b03', + 'Payload' => [ + 'Validated' => true, + 'Payment' => [ + 'Type' => 'CCA', + 'ProcessorTransactionId' => '4l7xg1WA7CS0YwgPgNZ0', + 'ExtendedData' => [ + 'CAVV' => 'AAABAWFlmQAAAABjRWWZEEFgFz8=', + 'ECIFlag' => '05', + 'XID' => 'NGw3eGcxV0E3Q1MwWXdnUGdOWjA=', + 'Enrolled' => 'Y', + 'PAResStatus' => 'Y', + 'SignatureVerification' => 'Y', + ], + ], + 'ActionCode' => 'SUCCESS', + 'ErrorNumber' => 0, + 'ErrorDescription' => 'Success', + ], +]; +$cardinalJWT = $jwtManagment->encode($response, $config->getApiKey()); + +return $cardinalJWT; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/AuthorizeCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/AuthorizeCommandTest.php new file mode 100644 index 0000000000000..f41025e08ec22 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/AuthorizeCommandTest.php @@ -0,0 +1,186 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetCardinal\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; + +/** + * Tests "Authorize" command for Authorize.net payment requests with 3D-Secure. + */ +class AuthorizeCommandTest extends AbstractTest +{ + /** + * Tests Authorize command with enabled 3D secure and valid Cardinal response JWT. + * + * @magentoConfigFixture default_store three_d_secure/cardinal/enabled_authorizenet 1 + * @magentoConfigFixture default_store three_d_secure/cardinal/environment sandbox + * @magentoConfigFixture default_store three_d_secure/cardinal/api_key some_api_key + * @magentoConfigFixture default_store three_d_secure/cardinal/api_identifier some_api_identifier + * @magentoConfigFixture default_store three_d_secure/cardinal/org_unit_id some_org_unit_id + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * + * @magentoAppIsolation enabled + */ + public function testAuthorizeCommandWith3dSecure() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('authorize'); + + $order = include __DIR__ . '/../../Fixture/full_order_with_3dsecure.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../Fixture/expected_request/authorize.php'; + $response = include __DIR__ . '/../../Fixture/response/authorize.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute( + [ + 'payment' => $paymentDO, + 'amount' => 100.00 + ] + ); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'P', + 'cvvResultCode' => '', + 'cavvResultCode' => '', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('P', $payment->getCcAvsStatus()); + $this->assertFalse($payment->getData('is_transaction_closed')); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + } + + /** + * Tests Authorize command with enabled 3D secure and invalid Cardinal response JWT. + * + * @magentoConfigFixture default_store three_d_secure/cardinal/enabled_authorizenet 1 + * @magentoConfigFixture default_store three_d_secure/cardinal/environment sandbox + * @magentoConfigFixture default_store three_d_secure/cardinal/api_key some_api_key + * @magentoConfigFixture default_store three_d_secure/cardinal/api_identifier some_api_identifier + * @magentoConfigFixture default_store three_d_secure/cardinal/org_unit_id some_org_unit_id + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * + * @magentoAppIsolation enabled + */ + public function testAuthorizeCommandWithInvalidJwt() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('authorize'); + + $order = include __DIR__ . '/../../../AuthorizenetAcceptjs/_files/full_order.php'; + $payment = $order->getPayment(); + $payment->setAdditionalInformation('cardinalJWT', 'Invalid JWT'); + + $paymentDO = $this->paymentFactory->create($payment); + + $this->expectException(LocalizedException::class); + + $command->execute( + [ + 'payment' => $paymentDO, + 'amount' => 100.00 + ] + ); + } + + /** + * Tests Authorize command with disabled 3D secure. + * + * @magentoConfigFixture default_store three_d_secure/cardinal/enabled_authorizenet 0 + * @magentoConfigFixture default_store three_d_secure/cardinal/environment sandbox + * @magentoConfigFixture default_store three_d_secure/cardinal/api_key some_api_key + * @magentoConfigFixture default_store three_d_secure/cardinal/api_identifier some_api_identifier + * @magentoConfigFixture default_store three_d_secure/cardinal/org_unit_id some_org_unit_id + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * + * @magentoAppIsolation enabled + */ + public function testAuthorizeCommandWithDisabled3dSecure() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('authorize'); + + $order = include __DIR__ . '/../../Fixture/full_order_with_3dsecure.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../../AuthorizenetAcceptjs/_files/expected_request/authorize.php'; + $response = include __DIR__ . '/../../../AuthorizenetAcceptjs/_files/response/authorize.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute( + [ + 'payment' => $paymentDO, + 'amount' => 100.00 + ] + ); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('Y', $payment->getCcAvsStatus()); + $this->assertFalse($payment->getData('is_transaction_closed')); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/SaleCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/SaleCommandTest.php new file mode 100644 index 0000000000000..c22e1fceaa84f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetCardinal/Gateway/Command/SaleCommandTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetCardinal\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; + +/** + * Tests "Sale" command for Authorize.net payment requests with 3D-Secure. + */ +class SaleCommandTest extends AbstractTest +{ + /** + * Tests Sale command with enabled 3D secure and valid Cardinal response JWT. + * + * @magentoConfigFixture default_store three_d_secure/cardinal/enabled_authorizenet 1 + * @magentoConfigFixture default_store three_d_secure/cardinal/environment sandbox + * @magentoConfigFixture default_store three_d_secure/cardinal/api_key some_api_key + * @magentoConfigFixture default_store three_d_secure/cardinal/api_identifier some_api_identifier + * @magentoConfigFixture default_store three_d_secure/cardinal/org_unit_id some_org_unit_id + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * + * @magentoAppIsolation enabled + */ + public function testSaleCommandWith3dSecure() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('sale'); + + $order = include __DIR__ . '/../../Fixture/full_order_with_3dsecure.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../Fixture/expected_request/sale.php'; + $response = include __DIR__ . '/../../../AuthorizenetAcceptjs/_files/response/sale.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute( + [ + 'payment' => $paymentDO, + 'amount' => 100.00 + ] + ); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('Y', $payment->getCcAvsStatus()); + $this->assertFalse($payment->getData('is_transaction_closed')); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithCatalogPriceRuleCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithCatalogPriceRuleCalculatorTest.php index 3c756041eaccd..0162f70c76b26 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithCatalogPriceRuleCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithCatalogPriceRuleCalculatorTest.php @@ -10,6 +10,7 @@ * @codingStandardsIgnoreStart * @magentoDataFixtureBeforeTransaction Magento/Bundle/_files/PriceCalculator/dynamic_bundle_product_with_catalog_rule.php * @codingStandardsIgnoreEnd + * @magentoDbIsolation enabled * @magentoAppArea frontend */ class DynamicBundleWithCatalogPriceRuleCalculatorTest extends BundlePriceAbstract From b9a36c336f4b216fd7a5347a2d041d637c28b56f Mon Sep 17 00:00:00 2001 From: Lusine Papyan <Lusine_Papyan@epam.com> Date: Tue, 11 Jun 2019 23:40:04 +0400 Subject: [PATCH 43/78] MAGETWO-69893: Error appears when restricted user tries to add new category from product page - Updated automated test script --- .../Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index c084dd78b71be..cd401b7a4651a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -14,7 +14,7 @@ <title value="Adding new category from product page by restricted user"/> <description value="Adding new category from product page by restricted user"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-99063"/> + <testCaseId value="MC-17229"/> <useCaseId value="MAGETWO-69893"/> <group value="catalog"/> </annotations> From 3bc4a678c046294e34de239a04e7c2182536ca7e Mon Sep 17 00:00:00 2001 From: Lilit Sargsyan <Lilit_Sargsyan@epam.com> Date: Wed, 12 Jun 2019 14:04:54 +0400 Subject: [PATCH 44/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values - Updated automated test script. --- .../AdminConfigurableProductActionGroup.xml | 35 ++++++------- ...reateProductConfigurationsPanelSection.xml | 2 + ...bleProductAttributeValueUniquenessTest.xml | 50 +++++-------------- 3 files changed, 31 insertions(+), 56 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 8b409935eb5c8..0ad7860c4087f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -205,25 +205,22 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnThirdNextButton"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFourthNextButton"/> </actionGroup> - <actionGroup name="adminSelectAttributeForConfigurableProductFromPage"> - <arguments> - <argument name="productAttributeCode" type="string" defaultValue="{{dropdownProductAttribute.attribute_code}}"/> - </arguments> - <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickToFilterAttributeNames"/> - <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="waitForAttributeCodeInputBeVisible"/> - <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{productAttributeCode}}" stepKey="fillAttributeName"/> - <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="applyFilter"/> - <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="selectFilteredAttribute"/> - <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> - </actionGroup> - <actionGroup name="createAnOptionForAttribute"> - <arguments> - <argument name="optionName" type="string" defaultValue="opt1"/> - </arguments> - <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateFirstNewValue"/> - <fillField userInput="{{optionName}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewOption"/> - <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> - <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <actionGroup name="selectCreatedAttributeAndCreateTwoOptions" extends="addNewProductConfigurationAttribute"> + <remove keyForRemoval="clickOnNewAttribute"/> + <remove keyForRemoval="waitForIFrame"/> + <remove keyForRemoval="switchToNewAttributeIFrame"/> + <remove keyForRemoval="fillDefaultLabel"/> + <remove keyForRemoval="clickOnNewAttributePanel"/> + <remove keyForRemoval="waitForSaveAttribute"/> + <remove keyForRemoval="switchOutOfIFrame"/> + <remove keyForRemoval="waitForFilters"/> + <fillField userInput="{{attribute.attribute_code}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <fillField userInput="{{firstOption.label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/> + <fillField userInput="{{secondOption.label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/> + <remove keyForRemoval="clickOnSelectAll"/> + <remove keyForRemoval="clickOnSecondNextButton"/> + <remove keyForRemoval="clickOnThirdNextButton"/> + <remove keyForRemoval="clickOnFourthNextButton"/> </actionGroup> <actionGroup name="changeProductConfigurationsInGrid"> <arguments> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index a5e74145c9fec..7b36b4ac276d5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -22,7 +22,9 @@ <element name="selectAll" type="button" selector=".action-select-all"/> <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> + <element name="attributeNameInTitle" type="input" selector="//div[@class='attribute-entity-title-block']/div[@class='attribute-entity-title']"/> <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> + <element name="attributeNameWithError" type="text" selector="//li[@data-attribute-option-title='']/div[contains(@class,'admin__field admin__field-create-new _error')]"/> <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> <element name="attributeCheckboxByIndex" type="input" selector="li.attribute-option:nth-of-type({{var1}}) input" parameterized="true"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index 28d594dd072cc..41a88e32a7f2c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -54,43 +54,19 @@ <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations1"/> <waitForPageLoad stepKey="waitForSelectAttributesPage1"/> - <actionGroup ref="adminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute1"> - <argument name="attributeName" value="$$createProductAttribute.attribute_code$$"/> + <actionGroup ref="selectCreatedAttributeAndCreateTwoOptions" stepKey="selectCreatedAttributeAndCreateOptions"> + <argument name="attribute" value="dropdownProductAttribute"/> + <argument name="firstOption" value="productAttributeOption1"/> + <argument name="secondOption" value="productAttributeOption1"/> </actionGroup> - <actionGroup ref="createAnOptionForAttribute" stepKey="createOption1ForSelectedAttribute"> - <argument name="optionName" value="opt1"/> - </actionGroup> - <actionGroup ref="createAnOptionForAttribute" stepKey="createOption2ForSelectedAttribute"> - <argument name="optionName" value="opt2"/> - </actionGroup> - <!--Proceed generation--> - <comment userInput="Proceed generation" stepKey="proceedGeneration"/> - <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext1"/> - <waitForPageLoad stepKey="waitForStep3Page"/> - <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext2"/> - <waitForPageLoad stepKey="waitForStep4Page"/> - <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickGenerateProducts"/> - <waitForPageLoad stepKey="waitProductGeneration"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <waitForPageLoad stepKey="waitForProductSave"/> - <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> - <waitForPageLoad stepKey="waitForProductPage2"/> - <seeInCurrentUrl url="{{ProductCatalogPage.url}}" stepKey="seeInProductUrl"/> - <!--Add an option with existing name--> - <comment userInput="Add an option with existing name" stepKey="addAnOptionWithExistingName"/> - <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab2"/> - <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations2"/> - <waitForPageLoad stepKey="waitForSelectAttributesPage2"/> - <actionGroup ref="adminSelectAttributeForConfigurableProductFromPage" stepKey="selectCreatedAttribute2"> - <argument name="attributeName" value="$$createProductAttribute.attribute_code$$"/> - </actionGroup> - <actionGroup ref="createAnOptionForAttribute" stepKey="createAnOptionWithExistingName"> - <argument name="optionName" value="opt1"/> - </actionGroup> - <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextToSubmitOption"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <!--Assert Error message--> - <comment userInput="Assert Error message" stepKey="assertErrorMsg"/> - <see userInput="Attributes must have unique option values" stepKey="verifyErrorMessage"/> + <!--Check that system does not allow to save 2 options with same name--> + <comment userInput="Check that system does not allow to save 2 options with same name" stepKey="checkOptionNameUniqueness"/> + <seeElement selector="{{AdminCreateProductConfigurationsPanel.attributeNameWithError}}" stepKey="seeThatOptionWithSameNameIsNotSaved"/> + <!--Click next and assert error message--> + <comment userInput="Click next and assert error message" stepKey="clickNextAndAssertErrMssg"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext"/> + <waitForPageLoad time="10" stepKey="waitForPageLoad"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.attributeNameInTitle}}" stepKey="grabErrMsg"/> + <see userInput='The value of attribute "$grabErrMsg" must be unique' stepKey="verifyAttributesValueUniqueness"/> </test> </tests> From 236b467aa9ae3e47dea790043643b177b1a1af4a Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Wed, 12 Jun 2019 14:42:02 -0500 Subject: [PATCH 45/78] MAGETWO-99500: Configurable options attribute position is not saved correctly via API --- .../Model/LinkManagement.php | 15 +- .../Api/LinkManagementTest.php | 204 +++++++++++++++--- ...figurable_attributes_for_position_test.php | 101 +++++++++ ..._attributes_for_position_test_rollback.php | 41 ++++ 4 files changed, 327 insertions(+), 34 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 2f07f8b90ce7e..890564fdb303c 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -132,7 +132,7 @@ public function addChild($sku, $childSku) throw new StateException(__("The parent product doesn't have configurable product options.")); } - $attributeIds = []; + $attributeData = []; foreach ($configurableProductOptions as $configurableProductOption) { $attributeCode = $configurableProductOption->getProductAttribute()->getAttributeCode(); if (!$child->getData($attributeCode)) { @@ -143,9 +143,11 @@ public function addChild($sku, $childSku) ) ); } - $attributeIds[] = $configurableProductOption->getAttributeId(); + $attributeData[$configurableProductOption->getAttributeId()] = [ + 'position' => $configurableProductOption->getPosition() + ]; } - $configurableOptionData = $this->getConfigurableAttributesData($attributeIds); + $configurableOptionData = $this->getConfigurableAttributesData($attributeData); /** @var \Magento\ConfigurableProduct\Helper\Product\Options\Factory $optionFactory */ $optionFactory = $this->getOptionsFactory(); @@ -211,16 +213,16 @@ private function getOptionsFactory() /** * Get Configurable Attribute Data * - * @param int[] $attributeIds + * @param int[] $attributeData * @return array */ - private function getConfigurableAttributesData($attributeIds) + private function getConfigurableAttributesData($attributeData) { $configurableAttributesData = []; $attributeValues = []; $attributes = $this->attributeFactory->create() ->getCollection() - ->addFieldToFilter('attribute_id', $attributeIds) + ->addFieldToFilter('attribute_id', array_keys($attributeData)) ->getItems(); foreach ($attributes as $attribute) { foreach ($attribute->getOptions() as $option) { @@ -237,6 +239,7 @@ private function getConfigurableAttributesData($attributeIds) 'attribute_id' => $attribute->getId(), 'code' => $attribute->getAttributeCode(), 'label' => $attribute->getStoreLabel(), + 'position' => $attributeData[$attribute->getId()]['position'], 'values' => $attributeValues, ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php index d899839d43d40..aa8f3b35669bb 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php @@ -6,11 +6,16 @@ */ namespace Magento\ConfigurableProduct\Api; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Model\Entity\Attribute\Option; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\TestCase\WebapiAbstract; -class LinkManagementTest extends \Magento\TestFramework\TestCase\WebapiAbstract +class LinkManagementTest extends WebapiAbstract { const SERVICE_NAME = 'configurableProductLinkManagementV1'; + const OPTION_SERVICE_NAME = 'configurableProductOptionRepositoryV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/configurable-products'; @@ -85,9 +90,27 @@ public function testAddChildFullRestCreation() $this->createConfigurableProduct($productSku); $attribute = $this->attributeRepository->get('catalog_product', 'test_configurable'); - $attributeValue = $attribute->getOptions()[1]->getValue(); - $this->addOptionToConfigurableProduct($productSku, $attribute->getAttributeId(), $attributeValue); - $this->createSimpleProduct($childSku, $attributeValue); + + $this->addOptionToConfigurableProduct( + $productSku, + $attribute->getAttributeId(), + [ + [ + 'value_index' => $attribute->getOptions()[1]->getValue() + ] + ] + ); + + $this->createSimpleProduct( + $childSku, + [ + [ + 'attribute_code' => 'test_configurable', + 'value' => $attribute->getOptions()[1]->getValue() + ] + ] + ); + $res = $this->addChild($productSku, $childSku); $this->assertTrue($res); @@ -103,10 +126,129 @@ public function testAddChildFullRestCreation() $this->assertTrue($added); // clean up products + + $this->deleteProduct($productSku); + $this->deleteProduct($childSku); + } + + /** + * Test if configurable option attribute positions are being preserved after simple products were assigned to a + * configurable product. + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php + */ + public function testConfigurableOptionPositionPreservation() + { + $productSku = 'configurable-product-sku'; + $childProductSkus = [ + 'simple-product-sku-1', + 'simple-product-sku-2' + ]; + $attributesToAdd = [ + 'custom_attr_1', + 'custom_attr_2', + ]; + + $this->createConfigurableProduct($productSku); + + $position = 0; + $attributeOptions = []; + foreach ($attributesToAdd as $attributeToAdd) { + /** @var Attribute $attribute */ + $attribute = $this->attributeRepository->get('catalog_product', $attributeToAdd); + + /** @var Option $options[] */ + $options = $attribute->getOptions(); + array_shift($options); + + $attributeOptions[$attributeToAdd] = $options; + + $valueIndexesData = []; + foreach ($options as $option) { + $valueIndexesData []['value_index']= $option->getValue(); + } + $this->addOptionToConfigurableProduct( + $productSku, + $attribute->getAttributeId(), + $valueIndexesData, + $position + ); + $position++; + } + + $this->assertArrayHasKey($attributesToAdd[0], $attributeOptions); + $this->assertArrayHasKey($attributesToAdd[1], $attributeOptions); + $this->assertCount(4, $attributeOptions[$attributesToAdd[0]]); + $this->assertCount(4, $attributeOptions[$attributesToAdd[1]]); + + $attributesBeforeAssign = $this->getConfigurableAttribute($productSku); + + $simpleProdsAttributeData = []; + foreach ($attributeOptions as $attributeCode => $options) { + $simpleProdsAttributeData [0][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[0]->getValue(), + ]; + $simpleProdsAttributeData [0][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[1]->getValue(), + ]; + $simpleProdsAttributeData [1][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[2]->getValue(), + ]; + $simpleProdsAttributeData [1][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[3]->getValue(), + ]; + } + + foreach ($childProductSkus as $childNum => $childSku) { + $this->createSimpleProduct($childSku, $simpleProdsAttributeData[$childNum]); + $res = $this->addChild($productSku, $childSku); + $this->assertTrue($res); + } + + $childProductsDiff = array_diff( + $childProductSkus, + array_column( + $this->getChildren($productSku), + 'sku' + ) + ); + $this->assertCount(0, $childProductsDiff, 'Added child product count mismatch expected result'); + + $attributesAfterAssign = $this->getConfigurableAttribute($productSku); + + $this->assertEquals( + $attributesBeforeAssign[0]['position'], + $attributesAfterAssign[0]['position'], + 'Product 1 attribute option position mismatch' + ); + $this->assertEquals( + $attributesBeforeAssign[1]['position'], + $attributesAfterAssign[1]['position'], + 'Product 2 attribute option position mismatch' + ); + + foreach ($childProductSkus as $childSku) { + $this->deleteProduct($childSku); + } + $this->deleteProduct($productSku); + } + + /** + * Delete product by SKU + * + * @param string $sku + * @return bool + */ + private function deleteProduct(string $sku): bool + { $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/products/' . $productSku, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + 'resourcePath' => '/V1/products/' . $sku, + 'httpMethod' => Request::HTTP_METHOD_DELETE ], 'soap' => [ 'service' => 'catalogProductRepositoryV1', @@ -114,19 +256,29 @@ public function testAddChildFullRestCreation() 'operation' => 'catalogProductRepositoryV1DeleteById', ], ]; - $this->_webApiCall($serviceInfo, ['sku' => $productSku]); + return $this->_webApiCall($serviceInfo, ['sku' => $sku]); + } + + /** + * Get configurable product attributes + * + * @param string $productSku + * @return array + */ + protected function getConfigurableAttribute(string $productSku): array + { $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/products/' . $childSku, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + 'resourcePath' => self::RESOURCE_PATH . '/' . $productSku . '/options/all', + 'httpMethod' => Request::HTTP_METHOD_GET ], 'soap' => [ - 'service' => 'catalogProductRepositoryV1', + 'service' => self::OPTION_SERVICE_NAME, 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => 'catalogProductRepositoryV1DeleteById', - ], + 'operation' => self::OPTION_SERVICE_NAME . 'GetList' + ] ]; - $this->_webApiCall($serviceInfo, ['sku' => $childSku]); + return $this->_webApiCall($serviceInfo, ['sku' => $productSku]); } private function addChild($productSku, $childSku) @@ -134,7 +286,7 @@ private function addChild($productSku, $childSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $productSku . '/child', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + 'httpMethod' => Request::HTTP_METHOD_POST ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -159,7 +311,7 @@ protected function createConfigurableProduct($productSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + 'httpMethod' => Request::HTTP_METHOD_POST ], 'soap' => [ 'service' => 'catalogProductRepositoryV1', @@ -170,24 +322,22 @@ protected function createConfigurableProduct($productSku) return $this->_webApiCall($serviceInfo, $requestData); } - protected function addOptionToConfigurableProduct($productSku, $attributeId, $attributeValue) + protected function addOptionToConfigurableProduct($productSku, $attributeId, $attributeValues, $position = 0) { $requestData = [ 'sku' => $productSku, 'option' => [ 'attribute_id' => $attributeId, 'label' => 'test_configurable', - 'position' => 0, + 'position' => $position, 'is_use_default' => true, - 'values' => [ - ['value_index' => $attributeValue], - ] + 'values' => $attributeValues ] ]; $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/configurable-products/'. $productSku .'/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => 'configurableProductOptionRepositoryV1', @@ -198,7 +348,7 @@ protected function addOptionToConfigurableProduct($productSku, $attributeId, $at return $this->_webApiCall($serviceInfo, $requestData); } - protected function createSimpleProduct($sku, $attributeValue) + protected function createSimpleProduct($sku, $customAttributes) { $requestData = [ 'product' => [ @@ -209,15 +359,13 @@ protected function createSimpleProduct($sku, $attributeValue) 'price' => 3.62, 'status' => 1, 'visibility' => 4, - 'custom_attributes' => [ - ['attribute_code' => 'test_configurable', 'value' => $attributeValue], - ] + 'custom_attributes' => $customAttributes ] ]; $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => 'catalogProductRepositoryV1', @@ -244,7 +392,7 @@ protected function removeChild($productSku, $childSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => sprintf($resourcePath, $productSku, $childSku), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + 'httpMethod' => Request::HTTP_METHOD_DELETE ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -265,7 +413,7 @@ protected function getChildren($productSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $productSku . '/children', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET + 'httpMethod' => Request::HTTP_METHOD_GET ], 'soap' => [ 'service' => self::SERVICE_NAME, diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php new file mode 100644 index 0000000000000..3f907c0ad0cb6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Eav\Model\Config; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; + +Bootstrap::getInstance()->reinitialize(); + +/** @var $eavConfig Config */ +$eavConfig = Bootstrap::getObjectManager()->get(Config::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +$attributesData = [ + [ + 'code' => 'custom_attr_1', + 'label' => 'custom_attr_1', + ], + [ + 'code' => 'custom_attr_2', + 'label' => 'custom_attr_2', + ], +]; + +foreach ($attributesData as $attributeData) { + $attribute = $eavConfig->getAttribute('catalog_product', $attributeData['code']); + + $eavConfig->clear(); + + + if (!$attribute->getId()) { + + /** @var $attribute Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => $attributeData['code'], + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => $attributeData['label'], + 'backend_type' => 'int', + 'option' => [ + 'value' => [ + 'option_0' => [ + $attributeData['label'] . ' Option 1' + ], + 'option_1' => [ + $attributeData['label'] . ' Option 2' + ], + 'option_2' => [ + $attributeData['label'] . ' Option 3' + ], + 'option_3' => [ + $attributeData['label'] . ' Option 4' + ] + ], + 'order' => [ + 'option_0' => 1, + 'option_1' => 2, + 'option_2' => 3, + 'option_3' => 4 + ], + ], + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); + } +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php new file mode 100644 index 0000000000000..c947eaafda393 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = Bootstrap::getObjectManager() + ->get(Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$attributesToDelete = [ + 'custom_attr_1', + 'custom_attr_2', +]; + +foreach ($attributesToDelete as $attributeToDelete) { + $eavConfig = Bootstrap::getObjectManager()->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeToDelete); + if ($attribute instanceof AbstractAttribute + && $attribute->getId() + ) { + $attribute->delete(); + } + $eavConfig->clear(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 129f24ffe075767a9e150e6d4c43285cd94c18df Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Wed, 12 Jun 2019 16:20:39 -0500 Subject: [PATCH 46/78] MAGETWO-99500: Configurable options attribute position is not saved correctly via API --- .../Test/Unit/Model/LinkManagementTest.php | 4 +++- .../Magento/ConfigurableProduct/Api/LinkManagementTest.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php index ad2fcd1e59360..b50e9794caac4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php @@ -158,7 +158,7 @@ public function testAddChild() ->getMock(); $optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class) ->disableOriginalConstructor() - ->setMethods(['getProductAttribute', 'getAttributeId']) + ->setMethods(['getProductAttribute', 'getPosition', 'getAttributeId']) ->getMock(); $productAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() @@ -216,6 +216,7 @@ public function testAddChild() $productAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn('color'); $simple->expects($this->any())->method('getData')->willReturn('color'); $optionMock->expects($this->any())->method('getAttributeId')->willReturn('1'); + $optionMock->expects($this->any())->method('getPosition')->willReturn('0'); $optionsFactoryMock->expects($this->any())->method('create')->willReturn([$optionMock]); $attributeFactoryMock->expects($this->any())->method('create')->willReturn($attributeMock); @@ -223,6 +224,7 @@ public function testAddChild() $attributeCollectionMock->expects($this->any())->method('addFieldToFilter')->willReturnSelf(); $attributeCollectionMock->expects($this->any())->method('getItems')->willReturn([$attributeMock]); + $attributeMock->expects($this->any())->method('getId')->willReturn(1); $attributeMock->expects($this->any())->method('getOptions')->willReturn([$attributeOptionMock]); $extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions'); diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php index aa8f3b35669bb..53c1bf08bb796 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php @@ -12,6 +12,9 @@ use Magento\Framework\Webapi\Rest\Request; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Class LinkManagementTest for testing ConfigurableProduct to SimpleProduct link functionality + */ class LinkManagementTest extends WebapiAbstract { const SERVICE_NAME = 'configurableProductLinkManagementV1'; From 54d2e318e2a676c9ab695779230df5b968cbd2c9 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Wed, 12 Jun 2019 16:53:52 -0500 Subject: [PATCH 47/78] MAGETWO-99500: Configurable options attribute position is not saved correctly via API --- .../Test/Unit/Model/LinkManagementTest.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php index b50e9794caac4..ae00399be46dc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php @@ -152,9 +152,13 @@ public function testAddChild() $extensionAttributesMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductExtension::class) ->disableOriginalConstructor() - ->setMethods([ - 'getConfigurableProductOptions', 'setConfigurableProductOptions', 'setConfigurableProductLinks' - ]) + ->setMethods( + [ + 'getConfigurableProductOptions', + 'setConfigurableProductOptions', + 'setConfigurableProductLinks' + ] + ) ->getMock(); $optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class) ->disableOriginalConstructor() From e4c7282464275c741b8ac5de3ed6d347b6c9b314 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Wed, 12 Jun 2019 17:31:25 -0500 Subject: [PATCH 48/78] MAGETWO-99500: Configurable options attribute position is not saved correctly via API --- .../Test/Unit/Model/LinkManagementTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php index ae00399be46dc..c385934352ab8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php @@ -149,7 +149,6 @@ public function testAddChild() ->disableOriginalConstructor() ->setMethods(['getId', 'getData']) ->getMock(); - $extensionAttributesMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductExtension::class) ->disableOriginalConstructor() ->setMethods( @@ -193,7 +192,6 @@ public function testAddChild() ->disableOriginalConstructor() ->setMethods(['getValue', 'getLabel']) ->getMock(); - $attributeCollectionMock = $this->getMockBuilder( \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection::class ) @@ -227,15 +225,11 @@ public function testAddChild() $attributeMock->expects($this->any())->method('getCollection')->willReturn($attributeCollectionMock); $attributeCollectionMock->expects($this->any())->method('addFieldToFilter')->willReturnSelf(); $attributeCollectionMock->expects($this->any())->method('getItems')->willReturn([$attributeMock]); - $attributeMock->expects($this->any())->method('getId')->willReturn(1); $attributeMock->expects($this->any())->method('getOptions')->willReturn([$attributeOptionMock]); - $extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions'); $extensionAttributesMock->expects($this->any())->method('setConfigurableProductLinks'); - $this->productRepository->expects($this->once())->method('save'); - $this->assertTrue(true, $this->object->addChild($productSku, $childSku)); } From 25b50ecd1b5f7dacee5c843f6a9d90dd45edb5a8 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Tue, 11 Jun 2019 15:37:35 -0500 Subject: [PATCH 49/78] MAGETWO-99736: Authorize.net - 3D Secure 2.0 Support for 2.3 - update composer hash and minor fixes --- .../Gateway/Request/Authorize3DSecureBuilder.php | 4 ++-- app/code/Magento/AuthorizenetCardinal/Model/Config.php | 2 ++ .../Test/Unit/Observer/DataAssignObserverTest.php | 2 +- app/code/Magento/AuthorizenetCardinal/composer.json | 1 + app/code/Magento/CardinalCommerce/Model/Config.php | 6 ++++-- app/code/Magento/CardinalCommerce/Model/JwtManagement.php | 2 +- .../Test/Unit/{ => Model}/JwtManagementTest.php | 0 app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml | 2 +- composer.lock | 6 +++--- 9 files changed, 15 insertions(+), 10 deletions(-) rename app/code/Magento/CardinalCommerce/Test/Unit/{ => Model}/JwtManagementTest.php (100%) diff --git a/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php index 3ff4f7dcea065..7e3d63d6f186e 100644 --- a/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php +++ b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php @@ -65,8 +65,8 @@ public function build(array $buildSubject): array if ($payment instanceof Payment) { $cardinalJwt = (string)$payment->getAdditionalInformation('cardinalJWT'); $jwtPayload = $this->jwtParser->execute($cardinalJwt); - $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag']; - $cavv = $jwtPayload['Payload']['Payment']['ExtendedData']['CAVV']; + $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag'] ?? ''; + $cavv = $jwtPayload['Payload']['Payment']['ExtendedData']['CAVV'] ?? ''; $data = [ 'transactionRequest' => [ 'cardholderAuthentication' => [ diff --git a/app/code/Magento/AuthorizenetCardinal/Model/Config.php b/app/code/Magento/AuthorizenetCardinal/Model/Config.php index a4390bb10369b..e70a6a2e39c1f 100644 --- a/app/code/Magento/AuthorizenetCardinal/Model/Config.php +++ b/app/code/Magento/AuthorizenetCardinal/Model/Config.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\AuthorizenetCardinal\Model; use Magento\Framework\App\Config\ScopeConfigInterface; diff --git a/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php b/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php index a45ddc6cfbb30..9f560507e34db 100644 --- a/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php +++ b/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php @@ -86,7 +86,7 @@ public function testDoesntSetDataWhenEmpty() /** * Tests case when CardinalCommerce is disabled. */ - public function testDoesnttDataWhenEmpty() + public function testDoesntSetDataWhenDisabled() { $config = $this->createMock(Config::class); $config->method('isActive') diff --git a/app/code/Magento/AuthorizenetCardinal/composer.json b/app/code/Magento/AuthorizenetCardinal/composer.json index e98e41551bcd3..2d3ceee209375 100644 --- a/app/code/Magento/AuthorizenetCardinal/composer.json +++ b/app/code/Magento/AuthorizenetCardinal/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/module-authorizenet-acceptjs": "*", + "magento/framework": "*", "magento/module-cardinal-commerce": "*", "magento/module-payment": "*", "magento/module-sales": "*", diff --git a/app/code/Magento/CardinalCommerce/Model/Config.php b/app/code/Magento/CardinalCommerce/Model/Config.php index 0975ed77c9708..64c72dae8d598 100644 --- a/app/code/Magento/CardinalCommerce/Model/Config.php +++ b/app/code/Magento/CardinalCommerce/Model/Config.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CardinalCommerce\Model; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -92,12 +94,12 @@ public function getOrgUnitId(?int $storeId = null): string */ public function getEnvironment(?int $storeId = null): string { - $orgUnitId = $this->scopeConfig->getValue( + $environment = $this->scopeConfig->getValue( 'three_d_secure/cardinal/environment', ScopeInterface::SCOPE_STORE, $storeId ); - return $orgUnitId; + return $environment; } /** diff --git a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php index cfbdb2d8a1f1b..953af1751fd65 100644 --- a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php +++ b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php @@ -53,7 +53,7 @@ public function decode(string $jwt, string $key): array throw new \InvalidArgumentException('Wrong number of segments in JWT'); } - list($headB64, $payloadB64, $signatureB64) = $parts; + [$headB64, $payloadB64, $signatureB64] = $parts; $headerJson = $this->urlSafeB64Decode($headB64); $header = $this->json->unserialize($headerJson); diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/JwtManagementTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php similarity index 100% rename from app/code/Magento/CardinalCommerce/Test/Unit/JwtManagementTest.php rename to app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml index ef44f31e08e69..4fa40436d4a90 100644 --- a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml +++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml @@ -36,7 +36,7 @@ <config_path>three_d_secure/cardinal/api_identifier</config_path> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> </field> - <field id="debug" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="debug" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Debug</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>three_d_secure/cardinal/debug</config_path> diff --git a/composer.lock b/composer.lock index 5454994ca82db..7eecee3b74aec 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": "912e04a44c38c8918799bd01b828fba0", + "content-hash": "cac9237019d018c1f7f9463414bc9bef", "packages": [ { "name": "braintree/braintree_php", @@ -2169,7 +2169,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -6771,7 +6771,7 @@ "time": "2019-04-29T20:56:26+00:00" }, { - "name": "mikey179/vfsStream", + "name": "mikey179/vfsstream", "version": "v1.6.5", "source": { "type": "git", From 1883c3f7a6c8f6f97d41a44ef426cc58c790fe69 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Thu, 13 Jun 2019 09:14:06 +0300 Subject: [PATCH 50/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../js/variations/steps/attributes_values.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index 9a2ef138919bc..6454dd61060f1 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -98,20 +98,20 @@ define([ }, /** - * @param {Object} option + * @param {Object} newOption * @return boolean */ - isValidOption: function (option) { + isValidOption: function (newOption) { var duplicatedOptions = [], errorOption, allOptions = []; - if (_.isEmpty(option.label)) { + if (_.isEmpty(newOption.label)) { return false; } _.each(this.options(), function (option) { - if (!_.isUndefined(allOptions[option.label])) { + if (!_.isUndefined(allOptions[option.label]) && (newOption.label === option.label)) { duplicatedOptions.push(option); } @@ -119,8 +119,10 @@ define([ }); if (duplicatedOptions.length) { - errorOption = $("[data-role=\"" + option.id + "\""); - errorOption.addClass("_error"); + _.each(duplicatedOptions, function (duplicatedOption) { + errorOption = $("[data-role=\"" + duplicatedOption.id + "\""); + errorOption.addClass("_error"); + }); return false; } return true; @@ -224,8 +226,9 @@ define([ if (option['is_new'] === true) { if (!attribute.isValidOption(option)) { - throw new Error($.mage.__( - 'The value of attribute ""%1"" must be unique').replace("\"%1\"", attribute.label) + throw new Error( + $.mage.__('The value of attribute ""%1"" must be unique') + .replace("\"%1\"", attribute.label) ); } From 5d060a07204c4f5d29f1cfcb46bfc6ddba03b524 Mon Sep 17 00:00:00 2001 From: Lilit Sargsyan <Lilit_Sargsyan@epam.com> Date: Thu, 13 Jun 2019 12:47:43 +0400 Subject: [PATCH 51/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values - Updated automated test script. --- .../Section/AdminCreateProductConfigurationsPanelSection.xml | 2 +- ...dminCheckConfigurableProductAttributeValueUniquenessTest.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index 7b36b4ac276d5..33fc25b983385 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -22,7 +22,7 @@ <element name="selectAll" type="button" selector=".action-select-all"/> <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> - <element name="attributeNameInTitle" type="input" selector="//div[@class='attribute-entity-title-block']/div[@class='attribute-entity-title']"/> + <element name="attributeNameInTitle" type="input" selector="//div[contains(@class,'attribute-entity-title-block')]/div[@class='attribute-entity-title']"/> <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> <element name="attributeNameWithError" type="text" selector="//li[@data-attribute-option-title='']/div[contains(@class,'admin__field admin__field-create-new _error')]"/> <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index 41a88e32a7f2c..df934446fc89b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -14,7 +14,7 @@ <title value="Attribute value validation (check for uniqueness) in configurable products"/> <description value="Attribute value validation (check for uniqueness) in configurable products"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-99519"/> + <testCaseId value="MC-17450"/> <useCaseId value="MAGETWO-99443"/> <group value="ConfigurableProduct"/> </annotations> From a3b3f55c67ef70c35680122613f374fa51fa07b5 Mon Sep 17 00:00:00 2001 From: Aliaksei Yakimovich2 <aliaksei_yakimovich2@epam.com> Date: Thu, 13 Jun 2019 17:31:31 +0300 Subject: [PATCH 52/78] MAGETWO-60918: Fatal error on Import/Export page if deleted category ids exists in category path - Added integrational test; --- .../Model/Export/ProductTest.php | 37 ++++++-- ...uct_export_with_broken_categories_path.php | 90 +++++++++++++++++++ ...t_with_broken_categories_path_rollback.php | 35 ++++++++ 3 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index c212d4c0d971a..183ba86ca7572 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types = 1); + namespace Magento\CatalogImportExport\Model\Export; /** @@ -248,9 +251,11 @@ public function testExceptionInGetExportData() */ public function testExportWithFieldsEnclosure() { - $this->model->setParameters([ - \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE => 1 - ]); + $this->model->setParameters( + [ + \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE => 1 + ] + ); $this->model->setWriter( $this->objectManager->create( @@ -278,11 +283,13 @@ public function testCategoryIdsFilter() ) ); - $this->model->setParameters([ - \Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP => [ - 'category_ids' => '2,13' + $this->model->setParameters( + [ + \Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP => [ + 'category_ids' => '2,13' + ] ] - ]); + ); $exportData = $this->model->export(); @@ -292,6 +299,22 @@ public function testCategoryIdsFilter() $this->assertNotContains('Simple Product Not Visible On Storefront', $exportData); } + /** + * Verify that export processed successfully with wrong category path + * + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_broken_categories_path.php + */ + public function testExportWithWrongCategoryPath() + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + + $this->model->export(); + } + /** * Test 'hide from product page' export for non-default store. * diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path.php new file mode 100644 index 0000000000000..3fdfeda826abb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) + ->getEntityType('catalog_product') + ->getDefaultAttributeSetId(); + +$productRepository = $objectManager->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class +); + +$categoryLinkRepository = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + [ + 'productRepository' => $productRepository + ] +); + +/** @var Magento\Catalog\Api\CategoryLinkManagementInterface $linkManagement */ +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +$reflectionClass = new \ReflectionClass(get_class($categoryLinkManagement)); +$properties = [ + 'productRepository' => $productRepository, + 'categoryLinkRepository' => $categoryLinkRepository +]; +foreach ($properties as $key => $value) { + if ($reflectionClass->hasProperty($key)) { + $reflectionProperty = $reflectionClass->getProperty($key); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($categoryLinkManagement, $value); + } +} + +/** + * After installation system has two categories: root one with ID:1 and Default category with ID:2 + */ +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(3) + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/3') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->save(); + +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(5) + ->setName('Category 1.1') + ->setParentId(3) + ->setPath('1/2/3/4/5') + ->setLevel(4) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setIsAnchor(true) + ->setPosition(1) + ->save(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [5] +); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php new file mode 100644 index 0000000000000..46a596addae59 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +// Remove products +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed +} + +//Remove categories +/** @var Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +foreach ($collection->addAttributeToFilter('level', ['in' => [2, 3, 4]]) as $category) { + /** @var \Magento\Catalog\Model\Category $category */ + $category->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From a52d069900777c291d27b2e20c122970b7aaecca Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@magento.com> Date: Thu, 13 Jun 2019 13:05:04 -0500 Subject: [PATCH 53/78] MAGETWO-99941: Configuarable product stock status stays 'In Stock' --- .../Observer/ParentItemProcessorInterface.php | 24 ++++ .../Observer/SaveInventoryDataObserver.php | 31 ++++- .../Model/Inventory/ParentItemProcessor.php | 123 ++++++++++++++++++ .../Magento/ConfigurableProduct/etc/di.xml | 7 + .../SaveInventoryDataObserverTest.php | 85 ++++++++++++ .../configurable_options_with_low_stock.php | 37 ++++++ 6 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php create mode 100644 app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php diff --git a/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php b/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php new file mode 100644 index 0000000000000..dd5689a396ebd --- /dev/null +++ b/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Observer; + +use Magento\Catalog\Api\Data\ProductInterface as Product; + +/** + * Interface for processing parent items of complex product types + */ +interface ParentItemProcessorInterface +{ + /** + * Process stock for parent items + * + * @param Product $product + * @return void + */ + public function process(Product $product); +} diff --git a/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php b/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php index 03ba58d3f4987..dd67140fa0c18 100644 --- a/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php @@ -13,6 +13,8 @@ use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\StockItemValidator; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; /** * Saves stock data from a product to the Stock Item @@ -39,6 +41,11 @@ class SaveInventoryDataObserver implements ObserverInterface */ private $stockItemValidator; + /** + * @var ParentItemProcessorInterface[] + */ + private $parentItemProcessorPool; + /** * @var array */ @@ -77,15 +84,18 @@ class SaveInventoryDataObserver implements ObserverInterface * @param StockConfigurationInterface $stockConfiguration * @param StockRegistryInterface $stockRegistry * @param StockItemValidator $stockItemValidator + * @param ParentItemProcessorInterface[] $parentItemProcessorPool */ public function __construct( StockConfigurationInterface $stockConfiguration, StockRegistryInterface $stockRegistry, - StockItemValidator $stockItemValidator = null + StockItemValidator $stockItemValidator = null, + array $parentItemProcessorPool = [] ) { $this->stockConfiguration = $stockConfiguration; $this->stockRegistry = $stockRegistry; $this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class); + $this->parentItemProcessorPool = $parentItemProcessorPool; } /** @@ -96,10 +106,15 @@ public function __construct( * * @param EventObserver $observer * @return void + * @throws LocalizedException + * @throws NoSuchEntityException */ public function execute(EventObserver $observer) { + /** @var Product $product */ $product = $observer->getEvent()->getProduct(); + + /** @var Item $stockItem */ $stockItem = $this->getStockItemToBeUpdated($product); if ($product->getStockData() !== null) { @@ -108,6 +123,7 @@ public function execute(EventObserver $observer) } $this->stockItemValidator->validate($product, $stockItem); $this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem); + $this->processParents($product); } /** @@ -156,4 +172,17 @@ private function getStockData(Product $product) } return $stockData; } + + /** + * Process stock data for parent products + * + * @param Product $product + * @return void + */ + private function processParents(Product $product) + { + foreach ($this->parentItemProcessorPool as $processor) { + $processor->process($product); + } + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php new file mode 100644 index 0000000000000..0812cef24a49a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Inventory; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Api\Data\ProductInterface as Product; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; + +/** + * Process parent stock item + */ +class ParentItemProcessor implements ParentItemProcessorInterface +{ + /** + * @var Configurable + */ + private $configurableType; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + public function __construct( + Configurable $configurableType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration + ) { + $this->configurableType = $configurableType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Process parent products + * + * @param Product $product + * @return void + */ + public function process(Product $product) + { + $parentIds = $this->configurableType->getParentIdsByChild($product->getId()); + foreach ($parentIds as $productId) { + $this->processStockForParent((int)$productId); + } + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + private function processStockForParent(int $productId) + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + + $childrenIds = $this->configurableType->getChildrenIds($productId); + $criteria->setProductsFilter($childrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $childrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $childrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) { + $parentStockItem->setIsInStock($childrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent( + StockItemInterface $parentStockItem, + bool $childrenIsInStock + ): bool { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index c3ffe988b00d7..8cec84abc4fea 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -255,4 +255,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="configurable" xsi:type="object"> Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php new file mode 100644 index 0000000000000..1b986b7670a1c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogInventory\Observer; + +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\StateException; +use Magento\Framework\Exception\CouldNotSaveException; + +/** + * Test for SaveInventoryDataObserver + */ +class SaveInventoryDataObserverTest extends TestCase +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + $this->stockItemRepository = Bootstrap::getObjectManager() + ->get(StockItemRepositoryInterface::class); + } + + /** + * Check that parent product will be out of stock + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/CatalogInventory/_files/configurable_options_with_low_stock.php + * @throws NoSuchEntityException + * @throws InputException + * @throws StateException + * @throws CouldNotSaveException + * @return void + */ + public function testAutoChangingIsInStockForParent() + { + /** @var ProductInterface $product */ + $product = $this->productRepository->get('simple_10'); + + /** @var ProductExtensionInterface $attributes*/ + $attributes = $product->getExtensionAttributes(); + + /** @var StockItemInterface $stockItem */ + $stockItem = $attributes->getStockItem(); + $stockItem->setQty(0); + $stockItem->setIsInStock(false); + $attributes->setStockItem($stockItem); + $product->setExtensionAttributes($attributes); + $this->productRepository->save($product); + + /** @var ProductInterface $product */ + $parentProduct = $this->productRepository->get('configurable'); + + $parentProductStockItem = $this->stockItemRepository->get( + $parentProduct->getExtensionAttributes()->getStockItem()->getItemId() + ); + $this->assertSame(false, $parentProductStockItem->getIsInStock()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php new file mode 100644 index 0000000000000..fc78b27ea59ce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/product_configurable.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var StockItemRepositoryInterface $stockItemRepository */ +$stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class); + +/** @var ProductInterface $product */ +$product = $productRepository->get('simple_10'); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setIsInStock(true) + ->setQty(1); +$stockItemRepository->save($stockItem); + +/** @var ProductInterface $product */ +$product = $productRepository->get('simple_20'); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setIsInStock(false) + ->setQty(0); +$stockItemRepository->save($stockItem); + From 3f99d89309d1d3a3959412b12a23775928ef9090 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@magento.com> Date: Thu, 13 Jun 2019 14:15:38 -0500 Subject: [PATCH 54/78] MAGETWO-99941: Configuarable product stock status stays 'In Stock' --- .../Model/Inventory/ParentItemProcessor.php | 6 ++++++ .../Observer/SaveInventoryDataObserverTest.php | 1 + .../_files/configurable_options_with_low_stock.php | 7 ++++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php index 0812cef24a49a..f1567f2b196de 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php @@ -40,6 +40,12 @@ class ParentItemProcessor implements ParentItemProcessorInterface */ private $stockConfiguration; + /** + * @param Configurable $configurableType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + */ public function __construct( Configurable $configurableType, StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php index 1b986b7670a1c..a50a7b096fe13 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php @@ -51,6 +51,7 @@ protected function setUp() * * @magentoAppArea adminhtml * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoDataFixture Magento/CatalogInventory/_files/configurable_options_with_low_stock.php * @throws NoSuchEntityException * @throws InputException diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php index fc78b27ea59ce..f6013b2e4b939 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php @@ -9,11 +9,13 @@ use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\StockItemRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; - -require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/product_configurable.php'; +use Magento\Catalog\Api\ProductRepositoryInterface; $objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + /** @var StockItemRepositoryInterface $stockItemRepository */ $stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class); @@ -34,4 +36,3 @@ $stockItem->setIsInStock(false) ->setQty(0); $stockItemRepository->save($stockItem); - From 3fce729e829966b9b20dfaf5761e4625614152e0 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov <evgeny_petrov@epam.com> Date: Thu, 13 Jun 2019 14:03:47 +0300 Subject: [PATCH 55/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values --- .../web/js/variations/steps/attributes_values.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index 6454dd61060f1..6c790c634ee93 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -111,7 +111,7 @@ define([ } _.each(this.options(), function (option) { - if (!_.isUndefined(allOptions[option.label]) && (newOption.label === option.label)) { + if (!_.isUndefined(allOptions[option.label]) && newOption.label === option.label) { duplicatedOptions.push(option); } @@ -120,11 +120,13 @@ define([ if (duplicatedOptions.length) { _.each(duplicatedOptions, function (duplicatedOption) { - errorOption = $("[data-role=\"" + duplicatedOption.id + "\""); - errorOption.addClass("_error"); + errorOption = $('[data-role="' + duplicatedOption.id + '"]'); + errorOption.addClass('_error'); }); + return false; } + return true; }, @@ -228,7 +230,7 @@ define([ if (!attribute.isValidOption(option)) { throw new Error( $.mage.__('The value of attribute ""%1"" must be unique') - .replace("\"%1\"", attribute.label) + .replace('"%1"', attribute.label) ); } From 43f5cdcf9ea86800e73faa15d72d1857b4c09b66 Mon Sep 17 00:00:00 2001 From: Lilit Sargsyan <Lilit_Sargsyan@epam.com> Date: Fri, 14 Jun 2019 08:33:31 +0400 Subject: [PATCH 56/78] MAGETWO-99443: Product attribute cannot be edited if the attribute was created with 2 same values - Updated automated test script. --- .../Section/AdminCreateProductConfigurationsPanelSection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index 33fc25b983385..a4c7d79d89c33 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -22,7 +22,7 @@ <element name="selectAll" type="button" selector=".action-select-all"/> <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> - <element name="attributeNameInTitle" type="input" selector="//div[contains(@class,'attribute-entity-title-block')]/div[@class='attribute-entity-title']"/> + <element name="attributeNameInTitle" type="input" selector="//div[contains(@class,'attribute-entity-title-block')]/div[contains(@class,'attribute-entity-title')]"/> <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> <element name="attributeNameWithError" type="text" selector="//li[@data-attribute-option-title='']/div[contains(@class,'admin__field admin__field-create-new _error')]"/> <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> From 3ae710c0efe84f1d19d8d4bc967f47891d19316f Mon Sep 17 00:00:00 2001 From: Sankalp Shekhar <shekhar.sankalp@gmail.com> Date: Fri, 14 Jun 2019 09:58:28 -0500 Subject: [PATCH 57/78] Move Quote related Plugins to correct module --- app/code/Magento/Quote/etc/webapi_rest/di.xml | 1 + app/code/Magento/Quote/etc/webapi_soap/di.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/code/Magento/Quote/etc/webapi_rest/di.xml b/app/code/Magento/Quote/etc/webapi_rest/di.xml index 2c8da7c775ec1..27d5ff7753425 100644 --- a/app/code/Magento/Quote/etc/webapi_rest/di.xml +++ b/app/code/Magento/Quote/etc/webapi_rest/di.xml @@ -11,5 +11,6 @@ </type> <type name="Magento\Quote\Model\QuoteRepository"> <plugin name="accessControl" type="Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl" /> + <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> </type> </config> diff --git a/app/code/Magento/Quote/etc/webapi_soap/di.xml b/app/code/Magento/Quote/etc/webapi_soap/di.xml index 2c8da7c775ec1..27d5ff7753425 100644 --- a/app/code/Magento/Quote/etc/webapi_soap/di.xml +++ b/app/code/Magento/Quote/etc/webapi_soap/di.xml @@ -11,5 +11,6 @@ </type> <type name="Magento\Quote\Model\QuoteRepository"> <plugin name="accessControl" type="Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl" /> + <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> </type> </config> From 9c1cb77ba2648f61d9b330d63b96660594d26afc Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <horytsky@adobe.com> Date: Fri, 14 Jun 2019 10:31:14 -0500 Subject: [PATCH 58/78] MAGETWO-99669: [Magento Cloud] Error on searching for < symbol --- .../Magento/Search/Model/ResourceModel/SynonymReader.php | 2 +- .../testsuite/Magento/Search/Model/SynonymReaderTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 45eee0a4001d1..1ac1547eb8d0a 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -106,7 +106,7 @@ private function queryByPhrase($phrase) */ private function escapePhrase(string $phrase): string { - return preg_replace('/@+|[@+-]+$/', '', $phrase); + return preg_replace('/@+|[@+-]+$|[<>]/', '', $phrase); } /** diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index 2d0020ba22680..9a6df9b9966d2 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -64,6 +64,15 @@ public static function loadByPhraseDataProvider() [ 'query_value+@', [] ], + [ + '<', [] + ], + [ + '>', [] + ], + [ + '<english>', [['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0]] + ], ]; } From 13ae59625cf6671e42089b0de5d1de564a31aa9c Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <horytsky@adobe.com> Date: Fri, 14 Jun 2019 13:25:36 -0500 Subject: [PATCH 59/78] MAGETWO-99669: [Magento Cloud] Error on searching for < symbol --- .../testsuite/Magento/Search/Model/SynonymReaderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index 9a6df9b9966d2..90540a1d637d5 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -25,7 +25,7 @@ protected function setUp() /** * @return array */ - public static function loadByPhraseDataProvider() + public function loadByPhraseDataProvider(): array { return [ [ @@ -81,7 +81,7 @@ public static function loadByPhraseDataProvider() * @param array $expectedResult * @dataProvider loadByPhraseDataProvider */ - public function testLoadByPhrase($phrase, $expectedResult) + public function testLoadByPhrase(string $phrase, array $expectedResult) { $data = $this->model->loadByPhrase($phrase)->getData(); From 32baf1968c487453d8f2a488f1cd6a31dae88393 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Sat, 15 Jun 2019 13:59:21 -0500 Subject: [PATCH 60/78] MC-17337: Braintree Error Code Mapping Not Working for CVV 200 --- .../Gateway/Validator/ErrorCodeProvider.php | 8 ++ .../Validator/ErrorCodeProviderTest.php | 94 +++++++++++++++++++ .../Braintree/etc/braintree_error_mapping.xml | 1 + 3 files changed, 103 insertions(+) create mode 100644 app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php diff --git a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php index 167fcb1569cbf..58ce33305da85 100644 --- a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php +++ b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php @@ -38,6 +38,14 @@ public function getErrorCodes($response): array $result[] = $error->code; } + if (isset($response->transaction) && $response->transaction->status === 'gateway_rejected') { + $result[] = $response->transaction->gatewayRejectionReason; + } + + if (isset($response->transaction) && $response->transaction->status === 'processor_declined') { + $result[] = $response->transaction->processorResponseCode; + } + return $result; } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php new file mode 100644 index 0000000000000..cddb4852da0e3 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Gateway\Validator; + +use Braintree\Result\Error; +use Magento\Braintree\Gateway\Validator\ErrorCodeProvider; +use PHPUnit\Framework\TestCase; + +/** + * Class ErrorCodeProviderTest + */ +class ErrorCodeProviderTest extends TestCase +{ + /** + * @var ErrorCodeProvider + */ + private $model; + + /** + * Checks a extracting error codes from response. + * + * @param array $errors + * @param array $transaction + * @param array $expectedResult + * @return void + * @dataProvider getErrorCodeDataProvider + */ + public function testGetErrorCodes(array $errors, array $transaction, array $expectedResult): void + { + $response = new Error( + [ + 'errors' => ['errors' => $errors], + 'transaction' => $transaction, + ] + ); + $this->model = new ErrorCodeProvider(); + $actual = $this->model->getErrorCodes($response); + + $this->assertSame($expectedResult, $actual); + } + + /** + * Gets list of errors variations. + * + * @return array + */ + public function getErrorCodeDataProvider(): array + { + return [ + [ + 'errors' => [ + ['code' => 91734], + ['code' => 91504] + ], + 'transaction' => [ + 'status' => 'success', + ], + 'expectedResult' => ['91734', '91504'] + ], + [ + 'errors' => [], + 'transaction' => [ + 'status' => 'processor_declined', + 'processorResponseCode' => '1000' + ], + 'expectedResult' => ['1000'] + ], + [ + 'errors' => [], + 'transaction' => [ + 'status' => 'processor_declined', + 'processorResponseCode' => '1000' + ], + 'expectedResult' => ['1000'] + ], + [ + 'errors' => [ + ['code' => 91734], + ['code' => 91504] + ], + 'transaction' => [ + 'status' => 'processor_declined', + 'processorResponseCode' => '1000' + ], + 'expectedResult' => ['91734', '91504', '1000'] + ], + ]; + } +} diff --git a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml index 81da0a252e567..7155264b4e6ad 100644 --- a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml +++ b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml @@ -20,6 +20,7 @@ <message code="81716" translate="true">Credit card number must be 12-19 digits.</message> <message code="81723" translate="true">Cardholder name is too long.</message> <message code="81736" translate="true">CVV verification failed.</message> + <message code="cvv" translate="true">CVV verification failed.</message> <message code="81737" translate="true">Postal code verification failed.</message> <message code="81750" translate="true">Credit card number is prohibited.</message> <message code="81801" translate="true">Addresses must have at least one field filled in.</message> From 29cc561352cf0ed1b500cf463fa9b2b27aae9f30 Mon Sep 17 00:00:00 2001 From: Sankalp Shekhar <shekhar.sankalp@gmail.com> Date: Fri, 14 Jun 2019 09:58:28 -0500 Subject: [PATCH 61/78] Move Quote related Plugins to correct module --- app/code/Magento/Quote/etc/webapi_rest/di.xml | 1 + app/code/Magento/Quote/etc/webapi_soap/di.xml | 1 + app/code/Magento/Sales/etc/webapi_rest/di.xml | 3 --- app/code/Magento/Sales/etc/webapi_soap/di.xml | 3 --- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Quote/etc/webapi_rest/di.xml b/app/code/Magento/Quote/etc/webapi_rest/di.xml index 2c8da7c775ec1..27d5ff7753425 100644 --- a/app/code/Magento/Quote/etc/webapi_rest/di.xml +++ b/app/code/Magento/Quote/etc/webapi_rest/di.xml @@ -11,5 +11,6 @@ </type> <type name="Magento\Quote\Model\QuoteRepository"> <plugin name="accessControl" type="Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl" /> + <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> </type> </config> diff --git a/app/code/Magento/Quote/etc/webapi_soap/di.xml b/app/code/Magento/Quote/etc/webapi_soap/di.xml index 2c8da7c775ec1..27d5ff7753425 100644 --- a/app/code/Magento/Quote/etc/webapi_soap/di.xml +++ b/app/code/Magento/Quote/etc/webapi_soap/di.xml @@ -11,5 +11,6 @@ </type> <type name="Magento\Quote\Model\QuoteRepository"> <plugin name="accessControl" type="Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl" /> + <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index f2cbd14eb8042..5d7838297a7c7 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Quote\Model\QuoteRepository"> - <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> - </type> <type name="Magento\Sales\Model\ResourceModel\Order"> <plugin name="authorization" type="Magento\Sales\Model\ResourceModel\Order\Plugin\Authorization" /> </type> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index f2cbd14eb8042..5d7838297a7c7 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Quote\Model\QuoteRepository"> - <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> - </type> <type name="Magento\Sales\Model\ResourceModel\Order"> <plugin name="authorization" type="Magento\Sales\Model\ResourceModel\Order\Plugin\Authorization" /> </type> From 58677ab776a70b7884d11323b6ae81b8bd3938ca Mon Sep 17 00:00:00 2001 From: Aliaksei Yakimovich2 <aliaksei_yakimovich2@epam.com> Date: Mon, 17 Jun 2019 13:29:10 +0300 Subject: [PATCH 62/78] MAGETWO-60918: Fatal error on Import/Export page if deleted category ids exists in category path - Fixed static test; --- ...roduct_export_with_broken_categories_path_rollback.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php index 46a596addae59..d642fbf01c55b 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php @@ -16,12 +16,8 @@ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); -try { - $product = $productRepository->get('simple', false, null, true); - $productRepository->delete($product); -} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - //Product already removed -} +$product = $productRepository->get('simple', false, null, true); +$productRepository->delete($product); //Remove categories /** @var Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ From 17ce132e162d2c34e8dc103aa83e5e74e9bd566e Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Mon, 17 Jun 2019 12:45:43 -0500 Subject: [PATCH 63/78] MC-17502: Cannot place order using transparent-redirect based payment and multishipping - fix attaching form key on submit when one form contains another form --- lib/web/mage/common.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/web/mage/common.js b/lib/web/mage/common.js index 01f696ec1b7fc..53f5b74872192 100644 --- a/lib/web/mage/common.js +++ b/lib/web/mage/common.js @@ -18,10 +18,21 @@ define([ 'form', function (e) { var formKeyElement, + existingFormKeyElement, + isKeyPresentInForm, form = $(e.target), formKey = $('input[name="form_key"]').val(); - if (formKey && !form.find('input[name="form_key"]').length && form[0].method !== 'get') { + existingFormKeyElement = form.find('input[name="form_key"]'); + isKeyPresentInForm = existingFormKeyElement.length; + + /* Verifies that existing auto-added form key is a direct form child element, + protection from a case when one form contains another form. */ + if (isKeyPresentInForm && existingFormKeyElement.attr('auto-added-form-key') === '1') { + isKeyPresentInForm = form.find('> input[name="form_key"]').length; + } + + if (formKey && !isKeyPresentInForm && form[0].method !== 'get') { formKeyElement = document.createElement('input'); formKeyElement.setAttribute('type', 'hidden'); formKeyElement.setAttribute('name', 'form_key'); From ad56730bca3a96f7497e9a2c49091c44a677abae Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Mon, 17 Jun 2019 08:59:23 -0500 Subject: [PATCH 64/78] MC-17337: Braintree Error Code Mapping Not Working for CVV 200 - fix tests --- .../Gateway/Validator/GeneralResponseValidatorTest.php | 9 +++++++-- .../Checkout/Api/PaymentInformationManagementTest.php | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php index 4741a3ea38c6f..3a4e9d581dd06 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php @@ -14,6 +14,9 @@ use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Class GeneralResponseValidatorTest + */ class GeneralResponseValidatorTest extends \PHPUnit\Framework\TestCase { /** @@ -82,9 +85,11 @@ public function dataProviderTestValidate() { $successTransaction = new \stdClass(); $successTransaction->success = true; + $successTransaction->status = 'authorized'; $failureTransaction = new \stdClass(); $failureTransaction->success = false; + $failureTransaction->status = 'declined'; $failureTransaction->message = 'Transaction was failed.'; $errors = [ @@ -93,10 +98,10 @@ public function dataProviderTestValidate() 'code' => 81804, 'attribute' => 'base', 'message' => 'Cannot process transaction.' - ] + ], ] ]; - $errorTransaction = new Error(['errors' => $errors]); + $errorTransaction = new Error(['errors' => $errors, 'transaction' => ['status' => 'declined']]); return [ [ diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php index f6338c1ee1664..1266dc7bb8843 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php @@ -96,7 +96,7 @@ public function testSavePaymentInformationAndPlaceOrderWithErrors( array_push($errors['errors'], ['code' => $testErrorCode]); } - $response = new Error(['errors' => $errors]); + $response = new Error(['errors' => $errors, 'transaction' => ['status' => 'declined']]); $this->client->method('placeRequest') ->willReturn(['object' => $response]); From 8fe7c88bf68c7eec7eb761f7b7690d73a9bb2ec1 Mon Sep 17 00:00:00 2001 From: Alastair Mucklow <amucklow@strangerpixel.com> Date: Tue, 18 Jun 2019 15:09:12 +0100 Subject: [PATCH 65/78] magento/magento2#23074: Magento 2.3.1 - URL rewrite rules are not creating for product after update url key Loop through category store ids when regenerating product rewrite urls. --- .../Observer/UrlRewriteHandler.php | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index 8f89c3a2fcd1f..d29ace5bd942e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -138,19 +138,20 @@ public function generateProductUrlRewrites(Category $category): array $mergeDataProvider = clone $this->mergeDataProviderPrototype; $this->isSkippedProduct[$category->getEntityId()] = []; $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); - $storeId = (int)$category->getStoreId(); - if ($category->getChangedProductIds()) { - $this->generateChangedProductUrls($mergeDataProvider, $category, $storeId, $saveRewriteHistory); - } else { - $mergeDataProvider->merge( - $this->getCategoryProductsUrlRewrites( - $category, - $storeId, - $saveRewriteHistory, - $category->getEntityId() - ) - ); + foreach ($category->getStoreIds() as $storeId) { + if ($category->getChangedProductIds()) { + $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); + } else { + $mergeDataProvider->merge( + $this->getCategoryProductsUrlRewrites( + $category, + (int)$storeId, + $saveRewriteHistory, + $category->getEntityId() + ) + ); + } } foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { From 60b577e3fa998ab9b4b85b0fb486cf542d626168 Mon Sep 17 00:00:00 2001 From: Alastair Mucklow <amucklow@strangerpixel.com> Date: Tue, 18 Jun 2019 15:21:24 +0100 Subject: [PATCH 66/78] Use getCategoryStoreIds method --- .../Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index d29ace5bd942e..e1378f8f68142 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -139,7 +139,7 @@ public function generateProductUrlRewrites(Category $category): array $this->isSkippedProduct[$category->getEntityId()] = []; $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); - foreach ($category->getStoreIds() as $storeId) { + foreach ($this->getCategoryStoreIds($category) as $storeId) { if ($category->getChangedProductIds()) { $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); } else { From 32de2806b5ea38b8d0b424838a5424f8279a6276 Mon Sep 17 00:00:00 2001 From: Alastair Mucklow <amucklow@strangerpixel.com> Date: Tue, 18 Jun 2019 16:39:29 +0100 Subject: [PATCH 67/78] Only loop around getCategoryProductsUrlRewrites() --- .../Observer/UrlRewriteHandler.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index e1378f8f68142..22b52f8600c4e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -138,15 +138,19 @@ public function generateProductUrlRewrites(Category $category): array $mergeDataProvider = clone $this->mergeDataProviderPrototype; $this->isSkippedProduct[$category->getEntityId()] = []; $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); + $storeId = (int)$category->getStoreId(); - foreach ($this->getCategoryStoreIds($category) as $storeId) { - if ($category->getChangedProductIds()) { - $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); - } else { + if ($category->getChangedProductIds()) { + $this->generateChangedProductUrls($mergeDataProvider, $category, $storeId, $saveRewriteHistory); + } else { + $categoryStoreIds = $this->getCategoryStoreIds($category); + + foreach ($categoryStoreIds as $categoryStoreId) { + $this->isSkippedProduct[$category->getEntityId()] = []; $mergeDataProvider->merge( $this->getCategoryProductsUrlRewrites( $category, - (int)$storeId, + $categoryStoreId, $saveRewriteHistory, $category->getEntityId() ) From 0da86ab8610b690d7ac149059f9b7c36e136f914 Mon Sep 17 00:00:00 2001 From: Alastair Mucklow <amucklow@strangerpixel.com> Date: Tue, 18 Jun 2019 16:39:56 +0100 Subject: [PATCH 68/78] Filter the product collection by the store id passed in --- .../Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index 22b52f8600c4e..0708ce6d7d714 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -246,7 +246,7 @@ private function getCategoryProductsUrlRewrites( $productCollection = $this->productCollectionFactory->create(); $productCollection->addCategoriesFilter(['eq' => [$category->getEntityId()]]) - ->setStoreId($storeId) + ->addStoreFilter($storeId) ->addAttributeToSelect('name') ->addAttributeToSelect('visibility') ->addAttributeToSelect('url_key') From ad6a8edff4c664352f51111d37b1021848c8ff51 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Wed, 19 Jun 2019 12:29:38 -0500 Subject: [PATCH 69/78] MC-17337: Braintree Error Code Mapping Not Working for CVV 200 - fix tests --- .../Validator/GeneralResponseValidatorTest.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php index 3a4e9d581dd06..d966e4e3f10ec 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php @@ -64,11 +64,13 @@ public function testValidate(array $validationSubject, bool $isValid, $messages, $result = new Result($isValid, $messages); $this->resultInterfaceFactory->method('create') - ->with([ - 'isValid' => $isValid, - 'failsDescription' => $messages, - 'errorCodes' => $errorCodes - ]) + ->with( + [ + 'isValid' => $isValid, + 'failsDescription' => $messages, + 'errorCodes' => $errorCodes + ] + ) ->willReturn($result); $actual = $this->responseValidator->validate($validationSubject); From 2d0fb70cb99fe7866eee251376a2de08dd2eab0c Mon Sep 17 00:00:00 2001 From: Alastair Mucklow <amucklow@strangerpixel.com> Date: Thu, 20 Jun 2019 09:52:33 +0100 Subject: [PATCH 70/78] Repair failing unit test --- .../Test/Unit/Observer/UrlRewriteHandlerTest.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php index b18597a42bf94..06a89a9dd5ec0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php @@ -138,8 +138,14 @@ public function testGenerateProductUrlRewrites() ->willReturn(1); $category->expects($this->any()) ->method('getData') - ->with('save_rewrites_history') - ->willReturn(true); + ->withConsecutive( + [$this->equalTo('save_rewrites_history')], + [$this->equalTo('initial_setup_flag')] + ) + ->willReturnOnConsecutiveCalls( + true, + null + ); /* @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject $childCategory1 */ $childCategory1 = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) @@ -175,6 +181,7 @@ public function testGenerateProductUrlRewrites() ->method('addIdFilter') ->willReturnSelf(); $productCollection->expects($this->any())->method('setStoreId')->willReturnSelf(); + $productCollection->expects($this->any())->method('addStoreFilter')->willReturnSelf(); $productCollection->expects($this->any())->method('addAttributeToSelect')->willReturnSelf(); $iterator = new \ArrayIterator([]); $productCollection->expects($this->any())->method('getIterator')->will($this->returnValue($iterator)); From 95d2cd75345de4b7c4d17088e6d44c2f736ffd8b Mon Sep 17 00:00:00 2001 From: Pieter Hoste <hoste.pieter@gmail.com> Date: Thu, 20 Jun 2019 20:23:03 +0200 Subject: [PATCH 71/78] Fixes incorrect file reference in a comment in a .htaccess file. --- pub/media/.htaccess | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pub/media/.htaccess b/pub/media/.htaccess index d8793a891430a..d68d163d7a6b5 100644 --- a/pub/media/.htaccess +++ b/pub/media/.htaccess @@ -31,7 +31,7 @@ SetHandler default-handler RewriteCond %{REQUEST_FILENAME} !-f ############################################ -## rewrite everything else to index.php +## rewrite everything else to get.php RewriteRule .* ../get.php [L] </IfModule> From 165b2cdb5edda0a8fbdd48ec366ff12e8fc01f2e Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Thu, 20 Jun 2019 18:28:24 -0500 Subject: [PATCH 72/78] MAGETWO-99736: Authorize.net - 3D Secure 2.0 Support for 2.3 - update composer hash --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 2cdd7c80cf280..e4bf7ad1aafa0 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": "ddcaab80336d44c861e6763148e5f071", + "content-hash": "fd2f56861434bfe26aaa4811879b7b24", "packages": [ { "name": "braintree/braintree_php", From 96be0d79b8258e0fbb77800a6cd0cecb807551fe Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina <veronika_kurochkina@epam.com> Date: Fri, 21 Jun 2019 11:58:14 +0300 Subject: [PATCH 73/78] MAGETWO-63599: [GitHub] catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes #8204 - Static test fixes --- lib/internal/Magento/Framework/Image/Adapter/Gd2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index df6c7652758c3..d52e70ef56a11 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -325,8 +325,8 @@ public function checkAlpha($fileName) * * @param resource $imageResource * @param int $fileType - * @param bool &$isAlpha - * @param bool &$isTrueColor + * @param bool $isAlpha + * @param bool $isTrueColor * * @return boolean * From 2f35a3bf269c36bbead7475f780e6033edf84fe3 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Fri, 21 Jun 2019 12:19:50 +0300 Subject: [PATCH 74/78] magento/magento2#22675: Static test fix. --- app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js index 414c98a06be8e..845ed03725bfb 100644 --- a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js +++ b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js @@ -299,6 +299,7 @@ define([ // validate parent form if (self.$selector.validate().errorList.length) { $('body').trigger('processStop'); + return false; } From cd940528e47c840fdd97b01f2c29c0eccf747549 Mon Sep 17 00:00:00 2001 From: Yuliya Labudova <Yuliya_Labudova@epam.com> Date: Fri, 21 Jun 2019 14:33:05 +0300 Subject: [PATCH 75/78] MAGETWO-60918: Fatal error on Import/Export page if deleted category ids exists in category path - Fix fixture for integration test. --- ...export_with_broken_categories_path_rollback.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php index d642fbf01c55b..d03c95cbeb4ba 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_broken_categories_path_rollback.php @@ -16,8 +16,18 @@ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$product = $productRepository->get('simple', false, null, true); -$productRepository->delete($product); +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} //Remove categories /** @var Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ From da08b65c24b3df953b8da88e1bf91e9ee0db0224 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Fri, 21 Jun 2019 07:44:06 -0500 Subject: [PATCH 76/78] MC-17462: Empty Order creation with Braintree and multiple address checkout --- .../method-renderer/multishipping/cc-form.js | 9 +-- .../method-renderer/multishipping/paypal.js | 9 +-- .../set-payment-information-extended.js | 60 +++++++++++++++++++ .../web/js/action/set-payment-information.js | 49 ++------------- .../frontend/web/js/view/payment/iframe.js | 19 ++++-- 5 files changed, 87 insertions(+), 59 deletions(-) create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js index dc816c035a23d..868fe174ae482 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js @@ -12,7 +12,7 @@ define([ 'Magento_Ui/js/model/messageList', 'mage/translate', 'Magento_Checkout/js/model/full-screen-loader', - 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/action/set-payment-information-extended', 'Magento_Checkout/js/model/payment/additional-validators', 'Magento_Braintree/js/view/payment/validator-handler' ], function ( @@ -22,7 +22,7 @@ define([ messageList, $t, fullScreenLoader, - setPaymentInformationAction, + setPaymentInformationExtended, additionalValidators, validatorManager ) { @@ -51,9 +51,10 @@ define([ if (additionalValidators.validate()) { fullScreenLoader.startLoader(); $.when( - setPaymentInformationAction( + setPaymentInformationExtended( this.messageContainer, - this.getData() + this.getData(), + true ) ).done(this.done.bind(this)) .fail(this.fail.bind(this)); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js index 0a9ec4fb6c6ee..b3837103148cc 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js @@ -8,14 +8,14 @@ define([ 'jquery', 'underscore', 'Magento_Braintree/js/view/payment/method-renderer/paypal', - 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/action/set-payment-information-extended', 'Magento_Checkout/js/model/payment/additional-validators', 'Magento_Checkout/js/model/full-screen-loader' ], function ( $, _, Component, - setPaymentInformationAction, + setPaymentInformationExtended, additionalValidators, fullScreenLoader ) { @@ -131,9 +131,10 @@ define([ placeOrder: function () { fullScreenLoader.startLoader(); $.when( - setPaymentInformationAction( + setPaymentInformationExtended( this.messageContainer, - this.getData() + this.getData(), + true ) ).done(this.done.bind(this)) .fail(this.fail.bind(this)); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js new file mode 100644 index 0000000000000..4085da82f4151 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/url-builder', + 'mage/storage', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Customer/js/model/customer', + 'Magento_Checkout/js/action/get-totals', + 'Magento_Checkout/js/model/full-screen-loader' +], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { + 'use strict'; + + return function (messageContainer, paymentData, skipBilling) { + var serviceUrl, + payload; + + skipBilling = skipBilling || false; + payload = { + cartId: quote.getQuoteId(), + paymentMethod: paymentData + }; + + /** + * Checkout for guest and registered customer. + */ + if (!customer.isLoggedIn()) { + serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { + cartId: quote.getQuoteId() + }); + payload.email = quote.guestEmail; + } else { + serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); + } + + if (skipBilling === false) { + payload.billingAddress = quote.billingAddress(); + } + + fullScreenLoader.startLoader(); + + return storage.post( + serviceUrl, JSON.stringify(payload) + ).fail( + function (response) { + errorProcessor.process(response, messageContainer); + } + ).always( + function () { + fullScreenLoader.stopLoader(); + } + ); + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js index 997b60503a2b3..d5261c976a725 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js @@ -7,54 +7,13 @@ * @api */ define([ - 'Magento_Checkout/js/model/quote', - 'Magento_Checkout/js/model/url-builder', - 'mage/storage', - 'Magento_Checkout/js/model/error-processor', - 'Magento_Customer/js/model/customer', - 'Magento_Checkout/js/action/get-totals', - 'Magento_Checkout/js/model/full-screen-loader' -], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { + 'Magento_Checkout/js/action/set-payment-information-extended' + +], function (setPaymentInformationExtended) { 'use strict'; return function (messageContainer, paymentData) { - var serviceUrl, - payload; - - /** - * Checkout for guest and registered customer. - */ - if (!customer.isLoggedIn()) { - serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { - cartId: quote.getQuoteId() - }); - payload = { - cartId: quote.getQuoteId(), - email: quote.guestEmail, - paymentMethod: paymentData, - billingAddress: quote.billingAddress() - }; - } else { - serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); - payload = { - cartId: quote.getQuoteId(), - paymentMethod: paymentData, - billingAddress: quote.billingAddress() - }; - } - - fullScreenLoader.startLoader(); - return storage.post( - serviceUrl, JSON.stringify(payload) - ).fail( - function (response) { - errorProcessor.process(response, messageContainer); - } - ).always( - function () { - fullScreenLoader.stopLoader(); - } - ); + return setPaymentInformationExtended(messageContainer, paymentData, false); }; }); diff --git a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js index 1e352e4297131..089dabac00b0f 100644 --- a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js +++ b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js @@ -125,12 +125,7 @@ define([ this.isPlaceOrderActionAllowed(false); $.when( - setPaymentInformationAction( - this.messageContainer, - { - method: this.getCode() - } - ) + this.setPaymentInformation() ).done( this.done.bind(this) ).fail( @@ -145,6 +140,18 @@ define([ } }, + /** + * {Function} + */ + setPaymentInformation: function () { + setPaymentInformationAction( + this.messageContainer, + { + method: this.getCode() + } + ); + }, + /** * {Function} */ From f5ca2db1ca002cfb983ea0240bfffba117369439 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Sat, 22 Jun 2019 17:58:17 -0500 Subject: [PATCH 77/78] MAGETWO-99736: Authorize.net - 3D Secure 2.0 Support for 2.3 - update composer hash --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index e6af6bcfa840a..413e700c87b15 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": "457d0a686c8ece954ab4055da896aad2", + "content-hash": "ad16b0301a9bb01887ee4504b5f6a842", "packages": [ { "name": "braintree/braintree_php", @@ -2395,7 +2395,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", From 84dba5cd4e4772318fd5e2b3fa7cb6253c2b54f2 Mon Sep 17 00:00:00 2001 From: Veronika Kurochkina <veronika_kurochkina@epam.com> Date: Mon, 24 Jun 2019 11:38:55 +0300 Subject: [PATCH 78/78] MAGETWO-93061: CMS page of second website with same URLkey as first website, show content of First website instead of second website content. --- app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php index b39a770a446e9..0faf62607f5c4 100644 --- a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php +++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php @@ -19,7 +19,7 @@ class UrlBuilder /** * @var \Magento\Framework\UrlInterface */ - protected $frontendUrlBuilder; + private $frontendUrlBuilder; /** * @var EncoderInterface