diff --git a/.gitignore b/.gitignore index 8831ad0a17c39..68d38d9ca7817 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ atlassian* /.php_cs /.php_cs.cache /grunt-config.json -/dev/tools/grunt/configs/local-themes.js /pub/media/*.* !/pub/media/.htaccess /pub/media/attribute/* diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php index 3d1570e5ddfe7..e0c173a4cbfec 100644 --- a/app/code/Magento/Backend/Block/Page/Footer.php +++ b/app/code/Magento/Backend/Block/Page/Footer.php @@ -40,7 +40,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -57,4 +57,12 @@ public function getMagentoVersion() { return $this->productMetadata->getVersion(); } + + /** + * @inheritdoc + */ + protected function getCacheLifetime() + { + return 3600 * 24 * 10; + } } diff --git a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php index e479e8f560dae..47a156c16ce3e 100644 --- a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php +++ b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Backend\Block\System\Store\Delete; +use Magento\Backup\Helper\Data as BackupHelper; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml cms block edit form * @@ -12,6 +15,25 @@ */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { + /** + * @var BackupHelper + */ + private $backup; + + /** + * @inheritDoc + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Data\FormFactory $formFactory, + array $data = [], + ?BackupHelper $backup = null + ) { + parent::__construct($context, $registry, $formFactory, $data); + $this->backup = $backup ?? ObjectManager::getInstance()->get(BackupHelper::class); + } + /** * Init form * @@ -25,7 +47,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritDoc */ protected function _prepareForm() { @@ -45,6 +67,12 @@ protected function _prepareForm() $fieldset->addField('item_id', 'hidden', ['name' => 'item_id', 'value' => $dataObject->getId()]); + $backupOptions = ['0' => __('No')]; + $backupSelected = '0'; + if ($this->backup->isEnabled()) { + $backupOptions['1'] = __('Yes'); + $backupSelected = '1'; + } $fieldset->addField( 'create_backup', 'select', @@ -52,8 +80,8 @@ protected function _prepareForm() 'label' => __('Create DB Backup'), 'title' => __('Create DB Backup'), 'name' => 'create_backup', - 'options' => ['1' => __('Yes'), '0' => __('No')], - 'value' => '1' + 'options' => $backupOptions, + 'value' => $backupSelected ] ); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php index 0beeb5168b6d1..a9be14b77b29c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php @@ -14,6 +14,7 @@ * Store controller * * @author Magento Core Team + * @SuppressWarnings(PHPMD.AllPurposeAction) */ abstract class Store extends Action { @@ -86,6 +87,8 @@ protected function createPage() * Backup database * * @return bool + * + * @deprecated Backup module is to be removed. */ protected function _backupDatabase() { diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php index 94dca327195f3..0edeb5565f288 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php @@ -7,6 +7,8 @@ use Magento\Backend\App\Action; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Backup\Helper\Data as Helper; +use Magento\Framework\App\ObjectManager; /** * Backup admin controller @@ -14,6 +16,7 @@ * @author Magento Core Team * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.AllPurposeAction) */ abstract class Index extends Action implements HttpGetActionInterface { @@ -51,6 +54,11 @@ abstract class Index extends Action implements HttpGetActionInterface */ protected $maintenanceMode; + /** + * @var Helper + */ + private $helper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry @@ -58,6 +66,7 @@ abstract class Index extends Action implements HttpGetActionInterface * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Backup\Model\BackupFactory $backupModelFactory * @param \Magento\Framework\App\MaintenanceMode $maintenanceMode + * @param Helper|null $helper */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -65,13 +74,27 @@ public function __construct( \Magento\Framework\Backup\Factory $backupFactory, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Backup\Model\BackupFactory $backupModelFactory, - \Magento\Framework\App\MaintenanceMode $maintenanceMode + \Magento\Framework\App\MaintenanceMode $maintenanceMode, + ?Helper $helper = null ) { $this->_coreRegistry = $coreRegistry; $this->_backupFactory = $backupFactory; $this->_fileFactory = $fileFactory; $this->_backupModelFactory = $backupModelFactory; $this->maintenanceMode = $maintenanceMode; + $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); parent::__construct($context); } + + /** + * @inheritDoc + */ + public function dispatch(\Magento\Framework\App\RequestInterface $request) + { + if (!$this->helper->isEnabled()) { + return $this->_redirect('*/*/disabled'); + } + + return parent::dispatch($request); + } } diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php new file mode 100644 index 0000000000000..f6fe430ae0838 --- /dev/null +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php @@ -0,0 +1,48 @@ +pageFactory = $pageFactory; + } + + /** + * @inheritDoc + */ + public function execute() + { + return $this->pageFactory->create(); + } +} diff --git a/app/code/Magento/Backup/Cron/SystemBackup.php b/app/code/Magento/Backup/Cron/SystemBackup.php index 750262ab1c14a..9502377a39d06 100644 --- a/app/code/Magento/Backup/Cron/SystemBackup.php +++ b/app/code/Magento/Backup/Cron/SystemBackup.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Store\Model\ScopeInterface; +/** + * Performs scheduled backup. + */ class SystemBackup { const XML_PATH_BACKUP_ENABLED = 'system/backup/enabled'; @@ -101,6 +104,10 @@ public function __construct( */ public function execute() { + if (!$this->_backupData->isEnabled()) { + return $this; + } + if (!$this->_scopeConfig->isSetFlag(self::XML_PATH_BACKUP_ENABLED, ScopeInterface::SCOPE_STORE)) { return $this; } diff --git a/app/code/Magento/Backup/Helper/Data.php b/app/code/Magento/Backup/Helper/Data.php index b0bc292ffe926..c6df6a7366852 100644 --- a/app/code/Magento/Backup/Helper/Data.php +++ b/app/code/Magento/Backup/Helper/Data.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backup\Helper; use Magento\Framework\App\Filesystem\DirectoryList; @@ -285,4 +288,14 @@ public function extractDataFromFilename($filename) return $result; } + + /** + * Is backup functionality enabled. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag('system/backup/functionality_enabled'); + } } diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php index 8fbd5da1c9842..bc458a0a8e4bf 100644 --- a/app/code/Magento/Backup/Model/Db.php +++ b/app/code/Magento/Backup/Model/Db.php @@ -5,11 +5,16 @@ */ namespace Magento\Backup\Model; +use Magento\Backup\Helper\Data as Helper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\RuntimeException; + /** * Database backup model * * @api * @since 100.0.2 + * @deprecated Backup module is to be removed. */ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface { @@ -33,16 +38,24 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface */ protected $_resource = null; + /** + * @var Helper + */ + private $helper; + /** * @param \Magento\Backup\Model\ResourceModel\Db $resourceDb * @param \Magento\Framework\App\ResourceConnection $resource + * @param Helper|null $helper */ public function __construct( \Magento\Backup\Model\ResourceModel\Db $resourceDb, - \Magento\Framework\App\ResourceConnection $resource + \Magento\Framework\App\ResourceConnection $resource, + ?Helper $helper = null ) { $this->_resourceDb = $resourceDb; $this->_resource = $resource; + $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); } /** @@ -63,6 +76,8 @@ public function getResource() } /** + * Tables list. + * * @return array */ public function getTables() @@ -71,6 +86,8 @@ public function getTables() } /** + * Command to recreate given table. + * * @param string $tableName * @param bool $addDropIfExists * @return string @@ -81,6 +98,8 @@ public function getTableCreateScript($tableName, $addDropIfExists = false) } /** + * Generate table's data dump. + * * @param string $tableName * @return string */ @@ -90,6 +109,8 @@ public function getTableDataDump($tableName) } /** + * Header for dumps. + * * @return string */ public function getHeader() @@ -98,6 +119,8 @@ public function getHeader() } /** + * Footer for dumps. + * * @return string */ public function getFooter() @@ -106,6 +129,8 @@ public function getFooter() } /** + * Get backup SQL. + * * @return string */ public function renderSql() @@ -124,13 +149,14 @@ public function renderSql() } /** - * Create backup and stream write to adapter - * - * @param \Magento\Framework\Backup\Db\BackupInterface $backup - * @return $this + * @inheritDoc */ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backup) { + if (!$this->helper->isEnabled()) { + throw new RuntimeException(__('Backup functionality is disabled')); + } + $backup->open(true); $this->getResource()->beginTransaction(); @@ -179,8 +205,6 @@ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backu $this->getResource()->commitTransaction(); $backup->close(); - - return $this; } /** diff --git a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php index b7dfb30c0a1b3..56a7ef42a0bc2 100644 --- a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php +++ b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php @@ -3,134 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backup\Test\Unit\Cron; +use Magento\Backup\Cron\SystemBackup; +use PHPUnit\Framework\TestCase; +use Magento\Backup\Helper\Data as Helper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -class SystemBackupTest extends \PHPUnit\Framework\TestCase +class SystemBackupTest extends TestCase { /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - private $objectManager; - - /** - * @var \Magento\Backup\Cron\SystemBackup - */ - private $systemBackup; - - /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $scopeConfigMock; - - /** - * @var \Magento\Backup\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - private $backupDataMock; - - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - private $registryMock; - - /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Helper|\PHPUnit_Framework_MockObject_MockObject */ - private $loggerMock; + private $helperMock; /** - * Filesystem facade - * - * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + * @var SystemBackup */ - private $filesystemMock; + private $cron; /** - * @var \Magento\Framework\Backup\Factory|\PHPUnit_Framework_MockObject_MockObject + * @inheritDoc */ - private $backupFactoryMock; - - /** - * @var \Magento\Framework\App\MaintenanceMode|\PHPUnit_Framework_MockObject_MockObject - */ - private $maintenanceModeMock; - - /** - * @var \Magento\Framework\Backup\Db|\PHPUnit_Framework_MockObject_MockObject - */ - private $backupDbMock; - - /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $objectManagerMock; - protected function setUp() { - $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) - ->getMock(); - $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->getMock(); - $this->backupDataMock = $this->getMockBuilder(\Magento\Backup\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) - ->getMock(); - $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->backupFactoryMock = $this->getMockBuilder(\Magento\Framework\Backup\Factory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->maintenanceModeMock = $this->getMockBuilder(\Magento\Framework\App\MaintenanceMode::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->backupDbMock = $this->getMockBuilder(\Magento\Framework\Backup\Db::class) - ->disableOriginalConstructor() - ->getMock(); - $this->backupDbMock->expects($this->any())->method('setBackupExtension')->willReturnSelf(); - $this->backupDbMock->expects($this->any())->method('setTime')->willReturnSelf(); - $this->backupDbMock->expects($this->any())->method('setBackupsDir')->willReturnSelf(); - - $this->objectManager = new ObjectManager($this); - $this->systemBackup = $this->objectManager->getObject( - \Magento\Backup\Cron\SystemBackup::class, - [ - 'backupData' => $this->backupDataMock, - 'coreRegistry' => $this->registryMock, - 'logger' => $this->loggerMock, - 'scopeConfig' => $this->scopeConfigMock, - 'filesystem' => $this->filesystemMock, - 'backupFactory' => $this->backupFactoryMock, - 'maintenanceMode' => $this->maintenanceModeMock, - ] - ); + $objectManager = new ObjectManager($this); + $this->helperMock = $this->getMockBuilder(Helper::class)->disableOriginalConstructor()->getMock(); + $this->cron = $objectManager->getObject(SystemBackup::class, ['backupData' => $this->helperMock]); } /** - * @expectedException \Exception + * Test that cron doesn't do anything if backups are disabled. */ - public function testExecuteThrowsException() + public function testDisabled() { - $type = 'db'; - $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(true); - - $this->scopeConfigMock->expects($this->once())->method('getValue') - ->with('system/backup/type', 'store') - ->willReturn($type); - - $this->backupFactoryMock->expects($this->once())->method('create')->willReturn($this->backupDbMock); - - $this->backupDbMock->expects($this->once())->method('create')->willThrowException(new \Exception); - - $this->backupDataMock->expects($this->never())->method('getCreateSuccessMessageByType')->with($type); - $this->loggerMock->expects($this->never())->method('info'); - - $this->systemBackup->execute(); + $this->helperMock->expects($this->any())->method('isEnabled')->willReturn(false); + $this->cron->execute(); } } diff --git a/app/code/Magento/Backup/Test/Unit/Model/DbTest.php b/app/code/Magento/Backup/Test/Unit/Model/DbTest.php deleted file mode 100644 index 0cab5f0ad1e99..0000000000000 --- a/app/code/Magento/Backup/Test/Unit/Model/DbTest.php +++ /dev/null @@ -1,243 +0,0 @@ -dbResourceMock = $this->getMockBuilder(DbResource::class) - ->disableOriginalConstructor() - ->getMock(); - $this->connectionResourceMock = $this->getMockBuilder(ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->objectManager = new ObjectManager($this); - $this->dbModel = $this->objectManager->getObject( - Db::class, - [ - 'resourceDb' => $this->dbResourceMock, - 'resource' => $this->connectionResourceMock - ] - ); - } - - public function testGetResource() - { - self::assertEquals($this->dbResourceMock, $this->dbModel->getResource()); - } - - public function testGetTables() - { - $tables = []; - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($tables); - - self::assertEquals($tables, $this->dbModel->getTables()); - } - - public function testGetTableCreateScript() - { - $tableName = 'some_table'; - $script = 'script'; - $this->dbResourceMock->expects($this->once()) - ->method('getTableCreateScript') - ->with($tableName, false) - ->willReturn($script); - - self::assertEquals($script, $this->dbModel->getTableCreateScript($tableName, false)); - } - - public function testGetTableDataDump() - { - $tableName = 'some_table'; - $dump = 'dump'; - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataDump') - ->with($tableName) - ->willReturn($dump); - - self::assertEquals($dump, $this->dbModel->getTableDataDump($tableName)); - } - - public function testGetHeader() - { - $header = 'header'; - $this->dbResourceMock->expects($this->once()) - ->method('getHeader') - ->willReturn($header); - - self::assertEquals($header, $this->dbModel->getHeader()); - } - - public function testGetFooter() - { - $footer = 'footer'; - $this->dbResourceMock->expects($this->once()) - ->method('getFooter') - ->willReturn($footer); - - self::assertEquals($footer, $this->dbModel->getFooter()); - } - - public function testRenderSql() - { - $header = 'header'; - $script = 'script'; - $tableName = 'some_table'; - $tables = [$tableName, $tableName]; - $dump = 'dump'; - $footer = 'footer'; - - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($tables); - $this->dbResourceMock->expects($this->once()) - ->method('getHeader') - ->willReturn($header); - $this->dbResourceMock->expects($this->exactly(2)) - ->method('getTableCreateScript') - ->with($tableName, true) - ->willReturn($script); - $this->dbResourceMock->expects($this->exactly(2)) - ->method('getTableDataDump') - ->with($tableName) - ->willReturn($dump); - $this->dbResourceMock->expects($this->once()) - ->method('getFooter') - ->willReturn($footer); - - self::assertEquals( - $header . $script . $dump . $script . $dump . $footer, - $this->dbModel->renderSql() - ); - } - - public function testCreateBackup() - { - /** @var BackupInterface|\PHPUnit_Framework_MockObject_MockObject $backupMock */ - $backupMock = $this->getMockBuilder(BackupInterface::class)->getMock(); - /** @var DataObject $tableStatus */ - $tableStatus = new DataObject(); - - $tableName = 'some_table'; - $tables = [$tableName]; - $header = 'header'; - $footer = 'footer'; - $dropSql = 'drop_sql'; - $createSql = 'create_sql'; - $beforeSql = 'before_sql'; - $afterSql = 'after_sql'; - $dataSql = 'data_sql'; - $foreignKeysSql = 'foreign_keys'; - $triggersSql = 'triggers_sql'; - $rowsCount = 2; - $dataLength = 1; - - $this->dbResourceMock->expects($this->once()) - ->method('beginTransaction'); - $this->dbResourceMock->expects($this->once()) - ->method('commitTransaction'); - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($tables); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDropSql') - ->willReturn($dropSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableCreateSql') - ->with($tableName, false) - ->willReturn($createSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataBeforeSql') - ->with($tableName) - ->willReturn($beforeSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataAfterSql') - ->with($tableName) - ->willReturn($afterSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataSql') - ->with($tableName, $rowsCount, 0) - ->willReturn($dataSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableStatus') - ->with($tableName) - ->willReturn($tableStatus); - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($createSql); - $this->dbResourceMock->expects($this->once()) - ->method('getHeader') - ->willReturn($header); - $this->dbResourceMock->expects($this->once()) - ->method('getTableHeader') - ->willReturn($header); - $this->dbResourceMock->expects($this->once()) - ->method('getFooter') - ->willReturn($footer); - $this->dbResourceMock->expects($this->once()) - ->method('getTableForeignKeysSql') - ->willReturn($foreignKeysSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableTriggersSql') - ->willReturn($triggersSql); - $backupMock->expects($this->once()) - ->method('open'); - $backupMock->expects($this->once()) - ->method('close'); - - $tableStatus->setRows($rowsCount); - $tableStatus->setDataLength($dataLength); - - $backupMock->expects($this->any()) - ->method('write') - ->withConsecutive( - [$this->equalTo($header)], - [$this->equalTo($header . $dropSql . "\n")], - [$this->equalTo($createSql . "\n")], - [$this->equalTo($beforeSql)], - [$this->equalTo($dataSql)], - [$this->equalTo($afterSql)], - [$this->equalTo($foreignKeysSql)], - [$this->equalTo($triggersSql)], - [$this->equalTo($footer)] - ); - - $this->dbModel->createBackup($backupMock); - } -} diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml index 4028452d04439..90f6fa861b40f 100644 --- a/app/code/Magento/Backup/etc/adminhtml/system.xml +++ b/app/code/Magento/Backup/etc/adminhtml/system.xml @@ -9,13 +9,21 @@
- + + + + Disabled by default for security reasons + Magento\Config\Model\Config\Source\Yesno + Magento\Config\Model\Config\Source\Yesno + + 1 + - + 1 diff --git a/app/code/Magento/Backup/etc/config.xml b/app/code/Magento/Backup/etc/config.xml new file mode 100644 index 0000000000000..fb0808983b9c8 --- /dev/null +++ b/app/code/Magento/Backup/etc/config.xml @@ -0,0 +1,16 @@ + + + + + + + 0 + + + + diff --git a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml new file mode 100644 index 0000000000000..3470f528e5ceb --- /dev/null +++ b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml @@ -0,0 +1,17 @@ + + + + + Backup functionality is disabled + + + + + + + diff --git a/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml b/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml new file mode 100644 index 0000000000000..a5308dce5cc52 --- /dev/null +++ b/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml @@ -0,0 +1,7 @@ + +

Backup functionality is currently disabled. Please use other means for backups

diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index abf434bc6da26..eaebd8492b0a1 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -7,7 +7,6 @@ define([ 'jquery', 'underscore', - 'mage/utils/wrapper', 'Magento_Checkout/js/view/payment/default', 'Magento_Braintree/js/view/payment/adapter', 'Magento_Checkout/js/model/quote', @@ -19,7 +18,6 @@ define([ ], function ( $, _, - wrapper, Component, Braintree, quote, @@ -105,6 +103,12 @@ define([ } }); + quote.shippingAddress.subscribe(function () { + if (self.isActive()) { + self.reInitPayPal(); + } + }); + // for each component initialization need update property this.isReviewRequired(false); this.initClientConfig(); @@ -222,9 +226,8 @@ define([ /** * Re-init PayPal Auth Flow - * @param {Function} callback - Optional callback */ - reInitPayPal: function (callback) { + reInitPayPal: function () { if (Braintree.checkout) { Braintree.checkout.teardown(function () { Braintree.checkout = null; @@ -235,17 +238,6 @@ define([ this.clientConfig.paypal.amount = this.grandTotalAmount; this.clientConfig.paypal.shippingAddressOverride = this.getShippingAddress(); - if (callback) { - this.clientConfig.onReady = wrapper.wrap( - this.clientConfig.onReady, - function (original, checkout) { - this.clientConfig.onReady = original; - original(checkout); - callback(); - }.bind(this) - ); - } - Braintree.setConfig(this.clientConfig); Braintree.setup(); }, @@ -428,19 +420,17 @@ define([ * Triggers when customer click "Continue to PayPal" button */ payWithPayPal: function () { - this.reInitPayPal(function () { - if (!additionalValidators.validate()) { - return; - } + if (!additionalValidators.validate()) { + return; + } - try { - Braintree.checkout.paypal.initAuthFlow(); - } catch (e) { - this.messageContainer.addErrorMessage({ - message: $t('Payment ' + this.getTitle() + ' can\'t be initialized.') - }); - } - }.bind(this)); + try { + Braintree.checkout.paypal.initAuthFlow(); + } catch (e) { + this.messageContainer.addErrorMessage({ + message: $t('Payment ' + this.getTitle() + ' can\'t be initialized.') + }); + } }, /** diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index 6f0cc04790cc2..72155d922a25f 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -143,6 +143,13 @@ + + + + Magento\Bundle\Model\ProductOptionProcessor + + + diff --git a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php index 91f3a785df36b..f04e9ac8611bc 100644 --- a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php +++ b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php @@ -3,13 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Captcha\Model\Customer\Plugin; use Magento\Captcha\Helper\Data as CaptchaHelper; -use Magento\Framework\Session\SessionManagerInterface; +use Magento\Customer\Controller\Ajax\Login; +use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Session\SessionManagerInterface; +/** + * The plugin for ajax login controller. + */ class AjaxLogin { /** @@ -61,14 +67,14 @@ public function __construct( } /** - * @param \Magento\Customer\Controller\Ajax\Login $subject + * Validates captcha during request execution. + * + * @param Login $subject * @param \Closure $proceed * @return $this - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function aroundExecute( - \Magento\Customer\Controller\Ajax\Login $subject, + Login $subject, \Closure $proceed ) { $captchaFormIdField = 'captcha_form_id'; @@ -93,26 +99,31 @@ public function aroundExecute( foreach ($this->formIds as $formId) { if ($formId === $loginFormId) { $captchaModel = $this->helper->getCaptcha($formId); + if ($captchaModel->isRequired($username)) { - $captchaModel->logAttempt($username); if (!$captchaModel->isCorrect($captchaString)) { $this->sessionManager->setUsername($username); - return $this->returnJsonError(__('Incorrect CAPTCHA')); + $captchaModel->logAttempt($username); + return $this->returnJsonError(__('Incorrect CAPTCHA'), true); } } + + $captchaModel->logAttempt($username); } } return $proceed(); } /** + * Gets Json response. * * @param \Magento\Framework\Phrase $phrase - * @return \Magento\Framework\Controller\Result\Json + * @param bool $isCaptchaRequired + * @return Json */ - private function returnJsonError(\Magento\Framework\Phrase $phrase): \Magento\Framework\Controller\Result\Json + private function returnJsonError(\Magento\Framework\Phrase $phrase, bool $isCaptchaRequired = false): Json { $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData(['errors' => true, 'message' => $phrase]); + return $resultJson->setData(['errors' => true, 'message' => $phrase, 'captcha' => $isCaptchaRequired]); } } diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php index ec2a49f3fc566..0764ea36897d1 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php @@ -158,7 +158,7 @@ public function testAroundExecuteIncorrectCaptcha() $this->resultJsonMock ->expects($this->once()) ->method('setData') - ->with(['errors' => true, 'message' => __('Incorrect CAPTCHA')]) + ->with(['errors' => true, 'message' => __('Incorrect CAPTCHA'), 'captcha' => true]) ->will($this->returnSelf()); $closure = function () { diff --git a/app/code/Magento/Captcha/etc/di.xml b/app/code/Magento/Captcha/etc/di.xml index 3a929f5e6cc00..83c4e8aa1e2c1 100644 --- a/app/code/Magento/Captcha/etc/di.xml +++ b/app/code/Magento/Captcha/etc/di.xml @@ -27,7 +27,7 @@ - + diff --git a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js index 3a235df73a916..52968e507e6bf 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js @@ -17,7 +17,7 @@ define([ imageSource: ko.observable(captchaData.imageSrc), visibility: ko.observable(false), captchaValue: ko.observable(null), - isRequired: captchaData.isRequired, + isRequired: ko.observable(captchaData.isRequired), isCaseSensitive: captchaData.isCaseSensitive, imageHeight: captchaData.imageHeight, refreshUrl: captchaData.refreshUrl, @@ -41,7 +41,7 @@ define([ * @return {Boolean} */ getIsVisible: function () { - return this.visibility; + return this.visibility(); }, /** @@ -55,14 +55,14 @@ define([ * @return {Boolean} */ getIsRequired: function () { - return this.isRequired; + return this.isRequired(); }, /** * @param {Boolean} flag */ setIsRequired: function (flag) { - this.isRequired = flag; + this.isRequired(flag); }, /** diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js index f80b2ab163ffd..f78b848312702 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js @@ -89,6 +89,13 @@ define([ return this.currentCaptcha !== null ? this.currentCaptcha.getIsRequired() : false; }, + /** + * @param {Boolean} flag + */ + setIsRequired: function (flag) { + this.currentCaptcha.setIsRequired(flag); + }, + /** * @return {Boolean} */ diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js index 7709febea60a3..a8efd0865bbb8 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js @@ -4,34 +4,44 @@ */ define([ - 'Magento_Captcha/js/view/checkout/defaultCaptcha', - 'Magento_Captcha/js/model/captchaList', - 'Magento_Customer/js/action/login' -], -function (defaultCaptcha, captchaList, loginAction) { - 'use strict'; - - return defaultCaptcha.extend({ - /** @inheritdoc */ - initialize: function () { - var self = this, - currentCaptcha; - - this._super(); - currentCaptcha = captchaList.getCaptchaByFormId(this.formId); - - if (currentCaptcha != null) { - currentCaptcha.setIsVisible(true); - this.setCurrentCaptcha(currentCaptcha); - - loginAction.registerLoginCallback(function (loginData) { - if (loginData['captcha_form_id'] && - loginData['captcha_form_id'] == self.formId //eslint-disable-line eqeqeq - ) { + 'underscore', + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Customer/js/action/login' + ], + function (_, defaultCaptcha, captchaList, loginAction) { + 'use strict'; + + return defaultCaptcha.extend({ + /** @inheritdoc */ + initialize: function () { + var self = this, + currentCaptcha; + + this._super(); + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + + loginAction.registerLoginCallback(function (loginData, response) { + if (!loginData['captcha_form_id'] || loginData['captcha_form_id'] !== self.formId) { + return; + } + + if (_.isUndefined(response) || !response.errors) { + return; + } + + // check if captcha should be required after login attempt + if (!self.isRequired() && response.captcha && self.isRequired() !== response.captcha) { + self.setIsRequired(response.captcha); + } + self.refresh(); - } - }); + }); + } } - } + }); }); -}); diff --git a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html index 575b3ca6f732e..8923c81bf4bb3 100644 --- a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html +++ b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html @@ -4,12 +4,14 @@ * See COPYING.txt for license details. */ --> + + +
- - +
diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 1e07c0cdd924e..44bf153f83697 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -43,6 +43,8 @@ public function getPositions(int $categoryId): array $categoryId )->order( 'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC ); return array_flip($connection->fetchCol($select)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php index 1ff3a1bae5c28..7ad8b1a0ab3f8 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php @@ -107,7 +107,7 @@ public function testGetPositions() $this->select->expects($this->once()) ->method('where') ->willReturnSelf(); - $this->select->expects($this->once()) + $this->select->expects($this->exactly(2)) ->method('order') ->willReturnSelf(); $this->select->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 12bc9acfa4c51..009cd690d4cd4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -15,6 +15,7 @@ use Magento\Catalog\Helper\ImageFactory; use Magento\Catalog\Api\Data\ProductRender\ImageInterface; use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Framework\View\DesignLoader; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -33,6 +34,9 @@ class ImageTest extends \PHPUnit\Framework\TestCase /** @var DesignInterface | \PHPUnit_Framework_MockObject_MockObject */ private $design; + /** @var DesignLoader | \PHPUnit_Framework_MockObject_MockObject*/ + private $designLoader; + /** @var Image */ private $model; @@ -60,13 +64,15 @@ public function setUp() ->getMock(); $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->design = $this->createMock(DesignInterface::class); + $this->designLoader = $this->createMock(DesignLoader::class); $this->model = new Image( $this->imageFactory, $this->state, $this->storeManager, $this->design, $this->imageInterfaceFactory, - $this->imageCodes + $this->imageCodes, + $this->designLoader ); } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 216bc16968fcb..524927ac1c4b4 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -17,12 +17,14 @@ use Magento\Framework\View\DesignInterface; use Magento\Store\Model\StoreManager; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\View\DesignLoader; /** * Collect enough information about image rendering on front * If you want to add new image, that should render on front you need * to configure this class in di.xml * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image implements ProductRenderCollectorInterface { @@ -51,6 +53,7 @@ class Image implements ProductRenderCollectorInterface /** * @var DesignInterface + * @deprecated 2.3.0 DesignLoader is used for design theme loading */ private $design; @@ -59,6 +62,11 @@ class Image implements ProductRenderCollectorInterface */ private $imageRenderInfoFactory; + /** + * @var DesignLoader + */ + private $designLoader; + /** * Image constructor. * @param ImageFactory $imageFactory @@ -67,6 +75,7 @@ class Image implements ProductRenderCollectorInterface * @param DesignInterface $design * @param ImageInterfaceFactory $imageRenderInfoFactory * @param array $imageCodes + * @param DesignLoader $designLoader */ public function __construct( ImageFactory $imageFactory, @@ -74,7 +83,8 @@ public function __construct( StoreManagerInterface $storeManager, DesignInterface $design, ImageInterfaceFactory $imageRenderInfoFactory, - array $imageCodes = [] + array $imageCodes = [], + DesignLoader $designLoader = null ) { $this->imageFactory = $imageFactory; $this->imageCodes = $imageCodes; @@ -82,6 +92,8 @@ public function __construct( $this->storeManager = $storeManager; $this->design = $design; $this->imageRenderInfoFactory = $imageRenderInfoFactory; + $this->designLoader = $designLoader ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(DesignLoader::class); } /** @@ -124,6 +136,8 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ } /** + * Callback for emulating image creation + * * Callback in which we emulate initialize default design theme, depends on current store, be settings store id * from render info * @@ -136,7 +150,7 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ public function emulateImageCreating(ProductInterface $product, $imageCode, $storeId, ImageInterface $image) { $this->storeManager->setCurrentStore($storeId); - $this->design->setDefaultDesignTheme(); + $this->designLoader->load(); $imageHelper = $this->imageFactory->create(); $imageHelper->init($product, $imageCode); diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index a802496d299b8..7d2c3699ee2c2 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -602,6 +602,13 @@ + + + + Magento\Catalog\Model\ProductOptionProcessor + + + diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml index 54b945b48c104..670a943da0aad 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml @@ -188,6 +188,15 @@ for( j in config[i].children ) { if(config[i].children[j].id) { newNode = new Ext.tree.TreeNode(config[i].children[j]); + + if (typeof newNode.ui.onTextChange === 'function') { + newNode.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + } node.appendChild(newNode); newNode.addListener('click', editSet.unregister); } @@ -195,13 +204,20 @@ } } } - } - editSet = function() { - return { - register : function(node) { - editSet.currentNode = node; - }, + + editSet = function () { + return { + register: function (node) { + editSet.currentNode = node; + if (typeof node.ui.onTextChange === 'function') { + node.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + }, unregister : function() { editSet.currentNode = false; @@ -293,6 +309,14 @@ allowDrag : true }); + if (typeof newNode.ui.onTextChange === 'function') { + newNode.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + TreePanels.root.appendChild(newNode); newNode.addListener('beforemove', editSet.groupBeforeMove); newNode.addListener('beforeinsert', editSet.groupBeforeInsert); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js index 6903a17bcdcca..a2804a8723ce0 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js @@ -469,26 +469,6 @@ define([ } }, - /** - * toggles Selects states (for IE) except those to be shown in popup - */ - /*_toggleSelectsExceptBlock: function(flag) { - if(Prototype.Browser.IE){ - if (this.blockForm) { - var states = new Array; - var selects = this.blockForm.getElementsByTagName("select"); - for(var i=0; i
isSaleable()): ?> - getTypeInstance()->hasRequiredOptions($_product)): ?> + getTypeInstance()->isPossibleBuyFromList($_product)): ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js index 4d883fb1373bd..3b7fac3d19ea5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js @@ -42,6 +42,9 @@ define([ listens: { email: 'emailHasChanged', emailFocused: 'validateEmail' + }, + ignoreTmpls: { + email: true } }, checkDelay: 2000, diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php index 890c9bf5eae52..6f57efad41e75 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php @@ -6,11 +6,12 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Delete image files. */ -class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory @@ -79,7 +80,7 @@ public function execute() $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); $dir = $filesystem->getDirectoryRead(DirectoryList::MEDIA); $filePath = $path . '/' . \Magento\Framework\File\Uploader::getCorrectFileName($file); - if ($dir->isFile($dir->getRelativePath($filePath))) { + if ($dir->isFile($dir->getRelativePath($filePath)) && !preg_match('#.htaccess#', $file)) { $this->getStorage()->deleteFile($filePath); } } diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 7f61ded7d8835..c237d0ea9963a 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -16,6 +16,7 @@ use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; use Magento\Store\Model\ScopeInterface as StoreScope; +use Magento\Framework\Encryption\Encryptor; /** * System configuration type @@ -70,6 +71,11 @@ class System implements ConfigTypeInterface */ private $availableDataScopes; + /** + * @var Encryptor + */ + private $encryptor; + /** * @param ConfigSourceInterface $source * @param PostProcessorInterface $postProcessor @@ -79,9 +85,11 @@ class System implements ConfigTypeInterface * @param PreProcessorInterface $preProcessor * @param int $cachingNestedLevel * @param string $configType - * @param Reader $reader + * @param Reader|null $reader + * @param Encryptor|null $encryptor * * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ConfigSourceInterface $source, @@ -92,17 +100,19 @@ public function __construct( PreProcessorInterface $preProcessor, $cachingNestedLevel = 1, $configType = self::CONFIG_TYPE, - Reader $reader = null + Reader $reader = null, + Encryptor $encryptor = null ) { $this->postProcessor = $postProcessor; $this->cache = $cache; $this->serializer = $serializer; $this->configType = $configType; $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); + $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(\Magento\Framework\Encryption\Encryptor::class); } /** - * Get config value by path. + * Get configuration value by path * * System configuration is separated by scopes (default, websites, stores). Configuration of a scope is inherited * from its parent scope (store inherits website). @@ -124,7 +134,7 @@ public function __construct( public function get($path = '') { if ($path === '') { - $this->data = array_replace_recursive($this->data, $this->loadAllData()); + $this->data = array_replace_recursive($this->loadAllData(), $this->data); return $this->data; } @@ -132,32 +142,6 @@ public function get($path = '') return $this->getWithParts($path); } - /** - * Merge newly loaded config data into already loaded. - * - * @param array $newData - * @return void - */ - private function mergeData(array $newData): void - { - if (array_key_exists(ScopeInterface::SCOPE_DEFAULT, $newData)) { - //Sometimes new data may contain links to arrays and we don't want that. - $this->data[ScopeInterface::SCOPE_DEFAULT] = (array)$newData[ScopeInterface::SCOPE_DEFAULT]; - unset($newData[ScopeInterface::SCOPE_DEFAULT]); - } - foreach ($newData as $scopeType => $scopeTypeData) { - if (!array_key_exists($scopeType, $this->data)) { - //Sometimes new data may contain links to arrays and we don't want that. - $this->data[$scopeType] = (array)$scopeTypeData; - } else { - foreach ($scopeTypeData as $scopeId => $scopeData) { - //Sometimes new data may contain links to arrays and we don't want that. - $this->data[$scopeType][$scopeId] = (array)$scopeData; - } - } - } - } - /** * Proceed with parts extraction from path. * @@ -170,10 +154,8 @@ private function getWithParts($path) if (count($pathParts) === 1 && $pathParts[0] !== ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$pathParts[0]])) { - //First filling data property with unprocessed data for post-processors to be able to use. $data = $this->readData(); - //Post-processing only the data we know is not yet processed. - $this->mergeData($this->postProcessor->process($data)); + $this->data = array_replace_recursive($data, $this->data); } return $this->data[$pathParts[0]]; @@ -183,11 +165,7 @@ private function getWithParts($path) if ($scopeType === ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$scopeType])) { - //Adding unprocessed data to the data property so it can be used in post-processing. - $this->mergeData($scopeData = $this->loadDefaultScopeData($scopeType)); - //Only post-processing the data we know is raw. - $scopeData = $this->postProcessor->process($scopeData); - $this->mergeData($scopeData); + $this->data = array_replace_recursive($this->loadDefaultScopeData($scopeType), $this->data); } return $this->getDataByPathParts($this->data[$scopeType], $pathParts); @@ -197,11 +175,10 @@ private function getWithParts($path) if (!isset($this->data[$scopeType][$scopeId])) { $scopeData = $this->loadScopeData($scopeType, $scopeId); - //Adding unprocessed data to the data property so it can be used in post-processing. - $this->mergeData($scopeData); - //Only post-processing the data we know is raw. - $scopeData = $this->postProcessor->process($scopeData); - $this->mergeData($scopeData); + + if (!isset($this->data[$scopeType][$scopeId])) { + $this->data = array_replace_recursive($scopeData, $this->data); + } } return isset($this->data[$scopeType][$scopeId]) @@ -221,11 +198,10 @@ private function loadAllData() if ($cachedData === false) { $data = $this->readData(); } else { - $data = $this->serializer->unserialize($cachedData); - $this->data = $data; + $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); } - return $this->postProcessor->process($data); + return $data; } /** @@ -242,7 +218,7 @@ private function loadDefaultScopeData($scopeType) $data = $this->readData(); $this->cacheData($data); } else { - $data = [$scopeType => $this->serializer->unserialize($cachedData)]; + $data = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; } return $data; @@ -263,7 +239,8 @@ private function loadScopeData($scopeType, $scopeId) if ($this->availableDataScopes === null) { $cachedScopeData = $this->cache->load($this->configType . '_scopes'); if ($cachedScopeData !== false) { - $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData); + $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); + $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); } } if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { @@ -272,14 +249,15 @@ private function loadScopeData($scopeType, $scopeId) $data = $this->readData(); $this->cacheData($data); } else { - $data = [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]]; + $serializedCachedData = $this->encryptor->decrypt($cachedData); + $data = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; } return $data; } /** - * Cache configuration data. + * Cache configuration data * * Caches data per scope to avoid reading data for all scopes on every request * @@ -289,12 +267,12 @@ private function loadScopeData($scopeType, $scopeId) private function cacheData(array $data) { $this->cache->save( - $this->serializer->serialize($data), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)), $this->configType, [self::CACHE_TAG] ); $this->cache->save( - $this->serializer->serialize($data['default']), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])), $this->configType . '_default', [self::CACHE_TAG] ); @@ -303,14 +281,14 @@ private function cacheData(array $data) foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) { $scopes[$curScopeType][$curScopeId] = 1; $this->cache->save( - $this->serializer->serialize($curScopeData), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)), $this->configType . '_' . $curScopeType . '_' . $curScopeId, [self::CACHE_TAG] ); } } $this->cache->save( - $this->serializer->serialize($scopes), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)), $this->configType . '_scopes', [self::CACHE_TAG] ); @@ -346,6 +324,9 @@ private function getDataByPathParts($data, $pathParts) private function readData(): array { $this->data = $this->reader->read(); + $this->data = $this->postProcessor->process( + $this->data + ); return $this->data; } diff --git a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php index 3b657dd1ab2d0..77110975401ff 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php @@ -70,4 +70,14 @@ public function getIdentities() } return $identities; } + + /** + * Get price for exact simple product added to cart + * + * @inheritdoc + */ + public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) + { + return parent::getProductPriceHtml($this->getChildProduct()); + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php index 1460cc516811e..3124a3b8cf0ed 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php @@ -41,6 +41,7 @@ protected function _construct() /** * Init select + * * @return $this|\Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection */ protected function _initSelect() @@ -73,9 +74,9 @@ public function setProductFilter($product) * * @return $this */ - protected function _beforeLoad() + protected function _renderFilters() { - parent::_beforeLoad(); + parent::_renderFilters(); $metadata = $this->getProductEntityMetadata(); $parentIds = []; foreach ($this->products as $product) { @@ -88,8 +89,7 @@ protected function _beforeLoad() } /** - * Retrieve is flat enabled flag - * Return always false if magento run admin + * Retrieve is flat enabled flag. Return always false if magento run admin * * @return bool */ diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index c93b216fc89d5..f44ba8a041cbb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -17,6 +17,9 @@ + + + diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 1dbb0969687d5..102ed1314f864 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -125,6 +125,13 @@ + + + + Magento\ConfigurableProduct\Model\ProductOptionProcessor + + + Magento\ConfigurableProduct\Pricing\Price\FinalPriceResolver diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php index 10fc2349968ea..e84da5b9fcd57 100644 --- a/app/code/Magento/Customer/Api/AccountManagementInterface.php +++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php @@ -31,13 +31,15 @@ interface AccountManagementInterface * @param \Magento\Customer\Api\Data\CustomerInterface $customer * @param string $password * @param string $redirectUrl + * @param string[] $extensions * @return \Magento\Customer\Api\Data\CustomerInterface * @throws \Magento\Framework\Exception\LocalizedException */ public function createAccount( \Magento\Customer\Api\Data\CustomerInterface $customer, $password = null, - $redirectUrl = '' + $redirectUrl = '', + $extensions = [] ); /** @@ -48,6 +50,7 @@ public function createAccount( * @param string $hash Password hash that we can save directly * @param string $redirectUrl URL fed to welcome email templates. Can be used by templates to, for example, direct * the customer to a product they were looking at after pressing confirmation link. + * @param string[] $extensions * @return \Magento\Customer\Api\Data\CustomerInterface * @throws \Magento\Framework\Exception\InputException If bad input is provided * @throws \Magento\Framework\Exception\State\InputMismatchException If the provided email is already used @@ -56,7 +59,8 @@ public function createAccount( public function createAccountWithPasswordHash( \Magento\Customer\Api\Data\CustomerInterface $customer, $hash, - $redirectUrl = '' + $redirectUrl = '', + $extensions = [] ); /** diff --git a/app/code/Magento/Customer/Controller/Ajax/Login.php b/app/code/Magento/Customer/Controller/Ajax/Login.php index 7d1e86c949792..6d0f8c036b290 100644 --- a/app/code/Magento/Customer/Controller/Ajax/Login.php +++ b/app/code/Magento/Customer/Controller/Ajax/Login.php @@ -107,7 +107,6 @@ public function __construct( /** * Get account redirect. - * For release backward compatibility. * * @deprecated 100.0.10 * @return AccountRedirect @@ -133,6 +132,8 @@ public function setAccountRedirect($value) } /** + * Initializes config dependency. + * * @deprecated 100.0.10 * @return ScopeConfigInterface */ @@ -145,6 +146,8 @@ protected function getScopeConfig() } /** + * Sets config dependency. + * * @deprecated 100.0.10 * @param ScopeConfigInterface $value * @return void @@ -199,25 +202,17 @@ public function execute() $response['redirectUrl'] = $this->_redirect->success($redirectRoute); $this->getAccountRedirect()->clearRedirectCookie(); } - } catch (EmailNotConfirmedException $e) { - $response = [ - 'errors' => true, - 'message' => $e->getMessage() - ]; - } catch (InvalidEmailOrPasswordException $e) { - $response = [ - 'errors' => true, - 'message' => $e->getMessage() - ]; - } catch (LocalizedException $e) { + } catch (LocalizedException | InvalidEmailOrPasswordException | EmailNotConfirmedException $e) { $response = [ 'errors' => true, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), + 'captcha' => $this->customerSession->getData('user_login_show_captcha') ]; } catch (\Exception $e) { $response = [ 'errors' => true, - 'message' => __('Invalid login or password.') + 'message' => __('Invalid login or password.'), + 'captcha' => $this->customerSession->getData('user_login_show_captcha') ]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 8beecffd1c865..0e2503c837d94 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -794,7 +794,7 @@ public function getConfirmationStatus($customerId) /** * @inheritdoc */ - public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '', $extensions = []) { if ($password !== null) { $this->checkPasswordStrength($password); @@ -810,7 +810,7 @@ public function createAccount(CustomerInterface $customer, $password = null, $re } else { $hash = null; } - return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl); + return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl, $extensions); } /** @@ -818,8 +818,12 @@ public function createAccount(CustomerInterface $customer, $password = null, $re * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function createAccountWithPasswordHash(CustomerInterface $customer, $hash, $redirectUrl = '') - { + public function createAccountWithPasswordHash( + CustomerInterface $customer, + $hash, + $redirectUrl = '', + $extensions = [] + ) { // This logic allows an existing customer to be added to a different store. No new account is created. // The plan is to move this logic into a new method called something like 'registerAccountWithStore' if ($customer->getId()) { @@ -892,7 +896,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash $customer = $this->customerRepository->getById($customer->getId()); $newLinkToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newLinkToken); - $this->sendEmailConfirmation($customer, $redirectUrl); + $this->sendEmailConfirmation($customer, $redirectUrl, $extensions); return $customer; } @@ -920,9 +924,10 @@ public function getDefaultShippingAddress($customerId) * * @param CustomerInterface $customer * @param string $redirectUrl + * @param array $extensions * @return void */ - protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl) + protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl, $extensions = []) { try { $hash = $this->customerRegistry->retrieveSecureData($customer->getId())->getPasswordHash(); @@ -932,7 +937,14 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU } elseif ($hash == '') { $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } - $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); + $this->getEmailNotification()->newAccount( + $customer, + $templateType, + $redirectUrl, + $customer->getStoreId(), + null, + $extensions + ); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); @@ -1384,6 +1396,7 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) $customerSecure->setRpTokenCreatedAt( $this->dateTimeFactory->create()->format(DateTime::DATETIME_PHP_FORMAT) ); + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); } return true; @@ -1537,4 +1550,15 @@ private function destroyCustomerSessions($customerId) $this->saveHandler->destroy($sessionId); } } + + /** + * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 4b65dcca0973f..30a9dbedde8d0 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -17,6 +17,8 @@ use Magento\Framework\Exception\LocalizedException; /** + * Class for notification customer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailNotification implements EmailNotificationInterface @@ -63,6 +65,8 @@ class EmailNotification implements EmailNotificationInterface self::NEW_ACCOUNT_EMAIL_CONFIRMATION => self::XML_PATH_CONFIRM_EMAIL_TEMPLATE, ]; + const CUSTOMER_CONFIRM_URL = 'customer/account/confirm/'; + /**#@-*/ /**#@-*/ @@ -362,6 +366,7 @@ public function passwordResetConfirmation(CustomerInterface $customer) * @param string $backUrl * @param string $storeId * @param string $sendemailStoreId + * @param array $extensions * @return void * @throws LocalizedException */ @@ -370,7 +375,8 @@ public function newAccount( $type = self::NEW_ACCOUNT_EMAIL_REGISTERED, $backUrl = '', $storeId = 0, - $sendemailStoreId = null + $sendemailStoreId = null, + $extensions = [] ) { $types = self::TEMPLATE_TYPES; @@ -388,11 +394,26 @@ public function newAccount( $customerEmailData = $this->getFullCustomerObject($customer); + $templateVars = [ + 'customer' => $customerEmailData, + 'back_url' => $backUrl, + 'store' => $store + ]; + if ($type == self::NEW_ACCOUNT_EMAIL_CONFIRMATION) { + if (empty($extensions)) { + $templateVars['url'] = self::CUSTOMER_CONFIRM_URL; + $templateVars['extensions'] = $extensions; + } else { + $templateVars['url'] = $extensions['url']; + $templateVars['extensions'] = $extensions['extension_info']; + } + } + $this->sendEmailTemplate( $customer, $types[$type], self::XML_PATH_REGISTER_EMAIL_IDENTITY, - ['customer' => $customerEmailData, 'back_url' => $backUrl, 'store' => $store], + $templateVars, $storeId ); } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index f510201559687..2eb1ef897e70e 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -151,7 +151,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) $customer->setConfirmation(null); } - $this->_validate($customer); + if (!$customer->getData('ignore_validation_flag')) { + $this->_validate($customer); + } return $this; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php index e55c5d443c9d1..96f47154e874e 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php @@ -23,41 +23,43 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $customer $defaultBillingId = $customer->getData('default_billing'); $defaultShippingId = $customer->getData('default_shipping'); - /** @var \Magento\Customer\Model\Address $address */ - foreach ($customer->getAddresses() as $address) { - if ($address->getData('_deleted')) { - if ($address->getId() == $defaultBillingId) { - $customer->setData('default_billing', null); - } + if (!$customer->getData('ignore_validation_flag')) { + /** @var \Magento\Customer\Model\Address $address */ + foreach ($customer->getAddresses() as $address) { + if ($address->getData('_deleted')) { + if ($address->getId() == $defaultBillingId) { + $customer->setData('default_billing', null); + } - if ($address->getId() == $defaultShippingId) { - $customer->setData('default_shipping', null); - } + if ($address->getId() == $defaultShippingId) { + $customer->setData('default_shipping', null); + } - $removedAddressId = $address->getId(); - $address->delete(); + $removedAddressId = $address->getId(); + $address->delete(); - // Remove deleted address from customer address collection - $customer->getAddressesCollection()->removeItemByKey($removedAddressId); - } else { - $address->setParentId( - $customer->getId() - )->setStoreId( - $customer->getStoreId() - )->setIsCustomerSaveTransaction( - true - )->save(); + // Remove deleted address from customer address collection + $customer->getAddressesCollection()->removeItemByKey($removedAddressId); + } else { + $address->setParentId( + $customer->getId() + )->setStoreId( + $customer->getStoreId() + )->setIsCustomerSaveTransaction( + true + )->save(); - if (($address->getIsPrimaryBilling() || - $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId - ) { - $customer->setData('default_billing', $address->getId()); - } + if (($address->getIsPrimaryBilling() || + $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId + ) { + $customer->setData('default_billing', $address->getId()); + } - if (($address->getIsPrimaryShipping() || - $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId - ) { - $customer->setData('default_shipping', $address->getId()); + if (($address->getIsPrimaryShipping() || + $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId + ) { + $customer->setData('default_shipping', $address->getId()); + } } } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index a053eee5cd09b..77049dac5aa0c 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -222,6 +222,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) ) { $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); } + $this->setValidationFlag($customerArr, $customerModel); $customerModel->save(); $this->customerRegistry->push($customerModel); $customerId = $customerModel->getId(); @@ -231,7 +232,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) ) { $customer->setAddresses($delegatedNewOperation->getCustomer()->getAddresses()); } - if ($customer->getAddresses() !== null) { + if ($customer->getAddresses() !== null && !$customerModel->getData('ignore_validation_flag')) { if ($customer->getId()) { $existingAddresses = $this->getById($customer->getId())->getAddresses(); $getIdFunc = function ($address) { @@ -396,4 +397,18 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti $collection->addFieldToFilter($fields); } } + + /** + * Set ignore_validation_flag to skip model validation + * + * @param array $customerArray + * @param Customer $customerModel + * @return void + */ + private function setValidationFlag($customerArray, $customerModel) + { + if (isset($customerArray['ignore_validation_flag'])) { + $customerModel->setData('ignore_validation_flag', true); + } + } } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php index aaaa799a5e26d..628727b202ba7 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php @@ -3,13 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** - * Test customer ajax login controller - */ namespace Magento\Customer\Test\Unit\Controller\Ajax; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Controller\Ajax\Login; +use Magento\Customer\Model\Account\Redirect; +use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\Result\Raw; +use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\ObjectManager\ObjectManager as FakeObjectManager; +use Magento\Framework\Stdlib\Cookie\CookieMetadata; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -17,223 +36,190 @@ class LoginTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Customer\Controller\Ajax\Login + * @var Login */ - protected $object; + private $controller; /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|MockObject */ - protected $request; + private $request; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseInterface|MockObject */ - protected $response; + private $response; /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - protected $customerSession; + private $customerSession; /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var FakeObjectManager|MockObject */ - protected $objectManager; + private $objectManager; /** - * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AccountManagement|MockObject */ - protected $customerAccountManagementMock; + private $accountManagement; /** - * @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var Data|MockObject */ - protected $jsonHelperMock; + private $jsonHelper; /** - * @var \Magento\Framework\Controller\Result\Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|MockObject */ - protected $resultJson; + private $resultJson; /** - * @var \Magento\Framework\Controller\Result\JsonFactory| \PHPUnit_Framework_MockObject_MockObject + * @var JsonFactory|MockObject */ - protected $resultJsonFactory; + private $resultJsonFactory; /** - * @var \Magento\Framework\Controller\Result\Raw| \PHPUnit_Framework_MockObject_MockObject + * @var Raw|MockObject */ - protected $resultRaw; + private $resultRaw; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var RedirectInterface|MockObject */ - protected $redirectMock; + private $redirect; /** - * @var \Magento\Framework\Stdlib\CookieManagerInterface| \PHPUnit_Framework_MockObject_MockObject + * @var CookieManagerInterface|MockObject */ private $cookieManager; /** - * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory| \PHPUnit_Framework_MockObject_MockObject + * @var CookieMetadataFactory|MockObject */ private $cookieMetadataFactory; /** - * @var \Magento\Framework\Stdlib\Cookie\CookieMetadata| \PHPUnit_Framework_MockObject_MockObject + * @inheritdoc */ - private $cookieMetadata; - - protected function setUp() + protected function setUp(): void { - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); $this->response = $this->createPartialMock( - \Magento\Framework\App\ResponseInterface::class, + ResponseInterface::class, ['setRedirect', 'sendResponse', 'representJson', 'setHttpResponseCode'] ); $this->customerSession = $this->createPartialMock( - \Magento\Customer\Model\Session::class, + Session::class, [ 'isLoggedIn', 'getLastCustomerId', 'getBeforeAuthUrl', 'setBeforeAuthUrl', 'setCustomerDataAsLoggedIn', - 'regenerateId' + 'regenerateId', + 'getData' ] ); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['get']); - $this->customerAccountManagementMock = - $this->createPartialMock(\Magento\Customer\Model\AccountManagement::class, ['authenticate']); + $this->objectManager = $this->createPartialMock(FakeObjectManager::class, ['get']); + $this->accountManagement = $this->createPartialMock(AccountManagement::class, ['authenticate']); - $this->jsonHelperMock = $this->createPartialMock(\Magento\Framework\Json\Helper\Data::class, ['jsonDecode']); + $this->jsonHelper = $this->createPartialMock(Data::class, ['jsonDecode']); - $this->resultJson = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $this->resultJson = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); - $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) + $this->resultJsonFactory = $this->getMockBuilder(JsonFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->cookieManager = $this->getMockBuilder(\Magento\Framework\Stdlib\CookieManagerInterface::class) + $this->cookieManager = $this->getMockBuilder(CookieManagerInterface::class) ->setMethods(['getCookie', 'deleteCookie']) ->getMockForAbstractClass(); - $this->cookieMetadataFactory = $this->getMockBuilder( - \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class - )->disableOriginalConstructor()->getMock(); - $this->cookieMetadata = $this->getMockBuilder(\Magento\Framework\Stdlib\Cookie\CookieMetadata::class) + $this->cookieMetadataFactory = $this->getMockBuilder(CookieMetadataFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->resultRaw = $this->getMockBuilder(\Magento\Framework\Controller\Result\Raw::class) + $this->resultRaw = $this->getMockBuilder(Raw::class) ->disableOriginalConstructor() ->getMock(); - $resultRawFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\RawFactory::class) + $resultRawFactory = $this->getMockBuilder(RawFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultRawFactory->expects($this->atLeastOnce()) - ->method('create') + $resultRawFactory->method('create') ->willReturn($this->resultRaw); - $contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); - $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $contextMock->expects($this->atLeastOnce())->method('getRedirect')->willReturn($this->redirectMock); - $contextMock->expects($this->atLeastOnce())->method('getRequest')->willReturn($this->request); - - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->object = $objectManager->getObject( - \Magento\Customer\Controller\Ajax\Login::class, + /** @var Context|MockObject $context */ + $context = $this->createMock(Context::class); + $this->redirect = $this->createMock(RedirectInterface::class); + $context->method('getRedirect') + ->willReturn($this->redirect); + $context->method('getRequest') + ->willReturn($this->request); + + $objectManager = new ObjectManager($this); + $this->controller = $objectManager->getObject( + Login::class, [ - 'context' => $contextMock, + 'context' => $context, 'customerSession' => $this->customerSession, - 'helper' => $this->jsonHelperMock, + 'helper' => $this->jsonHelper, 'response' => $this->response, 'resultRawFactory' => $resultRawFactory, 'resultJsonFactory' => $this->resultJsonFactory, 'objectManager' => $this->objectManager, - 'customerAccountManagement' => $this->customerAccountManagementMock, + 'customerAccountManagement' => $this->accountManagement, 'cookieManager' => $this->cookieManager, 'cookieMetadataFactory' => $this->cookieMetadataFactory ] ); } - public function testLogin() + /** + * Checks successful login. + */ + public function testLogin(): void { $jsonRequest = '{"username":"customer@example.com", "password":"password"}'; $loginSuccessResponse = '{"errors": false, "message":"Login successful."}'; + $this->withRequest($jsonRequest); - $this->request - ->expects($this->any()) - ->method('getContent') - ->willReturn($jsonRequest); - - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - - $this->request - ->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn(true); - - $this->resultJsonFactory->expects($this->atLeastOnce()) - ->method('create') + $this->resultJsonFactory->method('create') ->willReturn($this->resultJson); - $this->jsonHelperMock - ->expects($this->any()) - ->method('jsonDecode') + $this->jsonHelper->method('jsonDecode') ->with($jsonRequest) ->willReturn(['username' => 'customer@example.com', 'password' => 'password']); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerAccountManagementMock - ->expects($this->any()) - ->method('authenticate') + /** @var CustomerInterface|MockObject $customer */ + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->accountManagement->method('authenticate') ->with('customer@example.com', 'password') - ->willReturn($customerMock); + ->willReturn($customer); - $this->customerSession->expects($this->once()) - ->method('setCustomerDataAsLoggedIn') - ->with($customerMock); + $this->customerSession->method('setCustomerDataAsLoggedIn') + ->with($customer); + $this->customerSession->method('regenerateId'); - $this->customerSession->expects($this->once())->method('regenerateId'); + /** @var Redirect|MockObject $redirect */ + $redirect = $this->createMock(Redirect::class); + $this->controller->setAccountRedirect($redirect); + $redirect->method('getRedirectCookie') + ->willReturn('some_url1'); - $redirectMock = $this->createMock(\Magento\Customer\Model\Account\Redirect::class); - $this->object->setAccountRedirect($redirectMock); - $redirectMock->expects($this->once())->method('getRedirectCookie')->willReturn('some_url1'); + $this->withCookieManager(); - $this->cookieManager->expects($this->once()) - ->method('getCookie') - ->with('mage-cache-sessid') - ->willReturn(true); - $this->cookieMetadataFactory->expects($this->once()) - ->method('createCookieMetadata') - ->willReturn($this->cookieMetadata); - $this->cookieMetadata->expects($this->once()) - ->method('setPath') - ->with('/') - ->willReturnSelf(); - $this->cookieManager->expects($this->once()) - ->method('deleteCookie') - ->with('mage-cache-sessid', $this->cookieMetadata) - ->willReturnSelf(); + $this->withScopeConfig(); - $scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - $this->object->setScopeConfig($scopeConfigMock); - $scopeConfigMock->expects($this->once())->method('getValue') - ->with('customer/startup/redirect_dashboard') - ->willReturn(0); - - $this->redirectMock->expects($this->once())->method('success')->willReturn('some_url2'); - $this->resultRaw->expects($this->never())->method('setHttpResponseCode'); + $this->redirect->method('success') + ->willReturn('some_url2'); + $this->resultRaw->expects(self::never()) + ->method('setHttpResponseCode'); $result = [ 'errors' => false, @@ -241,67 +227,103 @@ public function testLogin() 'redirectUrl' => 'some_url2', ]; - $this->resultJson - ->expects($this->once()) - ->method('setData') + $this->resultJson->method('setData') ->with($result) ->willReturn($loginSuccessResponse); - $this->assertEquals($loginSuccessResponse, $this->object->execute()); + self::assertEquals($loginSuccessResponse, $this->controller->execute()); } - public function testLoginFailure() + /** + * Checks unsuccessful login. + */ + public function testLoginFailure(): void { $jsonRequest = '{"username":"invalid@example.com", "password":"invalid"}'; $loginFailureResponse = '{"message":"Invalid login or password."}'; + $this->withRequest($jsonRequest); - $this->request - ->expects($this->any()) - ->method('getContent') - ->willReturn($jsonRequest); - - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - - $this->request - ->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn(true); - - $this->resultJsonFactory->expects($this->once()) - ->method('create') + $this->resultJsonFactory->method('create') ->willReturn($this->resultJson); - $this->jsonHelperMock - ->expects($this->any()) - ->method('jsonDecode') + $this->jsonHelper->method('jsonDecode') ->with($jsonRequest) ->willReturn(['username' => 'invalid@example.com', 'password' => 'invalid']); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerAccountManagementMock - ->expects($this->any()) - ->method('authenticate') + /** @var CustomerInterface|MockObject $customer */ + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->accountManagement->method('authenticate') ->with('invalid@example.com', 'invalid') ->willThrowException(new InvalidEmailOrPasswordException(__('Invalid login or password.'))); - $this->customerSession->expects($this->never()) + $this->customerSession->expects(self::never()) ->method('setCustomerDataAsLoggedIn') - ->with($customerMock); - - $this->customerSession->expects($this->never())->method('regenerateId'); + ->with($customer); + $this->customerSession->expects(self::never()) + ->method('regenerateId'); + $this->customerSession->method('getData') + ->with('user_login_show_captcha') + ->willReturn(false); $result = [ 'errors' => true, - 'message' => __('Invalid login or password.') + 'message' => __('Invalid login or password.'), + 'captcha' => false ]; - $this->resultJson - ->expects($this->once()) - ->method('setData') + $this->resultJson->method('setData') ->with($result) ->willReturn($loginFailureResponse); - $this->assertEquals($loginFailureResponse, $this->object->execute()); + self::assertEquals($loginFailureResponse, $this->controller->execute()); + } + + /** + * Emulates request behavior. + * + * @param string $jsonRequest + */ + private function withRequest(string $jsonRequest): void + { + $this->request->method('getContent') + ->willReturn($jsonRequest); + + $this->request->method('getMethod') + ->willReturn('POST'); + + $this->request->method('isXmlHttpRequest') + ->willReturn(true); + } + + /** + * Emulates cookie manager behavior. + */ + private function withCookieManager(): void + { + $this->cookieManager->method('getCookie') + ->with('mage-cache-sessid') + ->willReturn(true); + $cookieMetadata = $this->getMockBuilder(CookieMetadata::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieMetadataFactory->method('createCookieMetadata') + ->willReturn($cookieMetadata); + $cookieMetadata->method('setPath') + ->with('/') + ->willReturnSelf(); + $this->cookieManager->method('deleteCookie') + ->with('mage-cache-sessid', $cookieMetadata) + ->willReturnSelf(); + } + + /** + * Emulates config behavior. + */ + private function withScopeConfig(): void + { + /** @var ScopeConfigInterface|MockObject $scopeConfig */ + $scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->controller->setScopeConfig($scopeConfig); + $scopeConfig->method('getValue') + ->with('customer/startup/redirect_dashboard') + ->willReturn(0); } } diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html index 010087ace2d42..9b183d63471f3 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html @@ -9,7 +9,9 @@ "var this.getUrl($store, 'customer/account/confirm/', [_query:[id:$customer.id, key:$customer.confirmation, back_url:$back_url]])":"Account Confirmation URL", "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var customer.email":"Customer Email", -"var customer.name":"Customer Name" +"var customer.name":"Customer Name", +"var extensions":"Extensions", +"var url":"Url" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +25,7 @@
- {{trans "Confirm Your Account"}} + {{trans "Confirm Your Account"}}
diff --git a/app/code/Magento/Customer/view/frontend/web/js/action/login.js b/app/code/Magento/Customer/view/frontend/web/js/action/login.js index d75b8f70c5346..0015e2732e383 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/action/login.js +++ b/app/code/Magento/Customer/view/frontend/web/js/action/login.js @@ -31,11 +31,11 @@ define([ if (response.errors) { messageContainer.addErrorMessage(response); callbacks.forEach(function (callback) { - callback(loginData); + callback(loginData, response); }); } else { callbacks.forEach(function (callback) { - callback(loginData); + callback(loginData, response); }); customerData.invalidate(['customer']); diff --git a/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js b/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js index c14a59af49706..37bd3a19df638 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js +++ b/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js @@ -84,7 +84,6 @@ define([ if (formElement.validation() && formElement.validation('isValid') ) { - this.isLoading(true); loginAction(loginData); } diff --git a/app/code/Magento/Downloadable/etc/di.xml b/app/code/Magento/Downloadable/etc/di.xml index 932e48e880880..4e9b0b55afb0b 100644 --- a/app/code/Magento/Downloadable/etc/di.xml +++ b/app/code/Magento/Downloadable/etc/di.xml @@ -77,6 +77,13 @@
+ + + + Magento\Downloadable\Model\ProductOptionProcessor + + + diff --git a/app/code/Magento/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index 5e2e2716e13d2..1b2cac32598e1 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/File.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/File.php @@ -120,8 +120,7 @@ public function extractValue(RequestInterface $request) } /** - * Validate file by attribute validate rules - * Return array of errors + * Validate file by attribute validate rules and return array of errors * * @param array $value * @return string[] @@ -147,7 +146,7 @@ protected function _validateByRules($value) return $this->_fileValidator->getMessages(); } - if (!is_uploaded_file($value['tmp_name'])) { + if (!empty($value['tmp_name']) && !is_uploaded_file($value['tmp_name'])) { return [__('"%1" is not a valid file.', $label)]; } @@ -174,12 +173,22 @@ public function validateValue($value) if ($this->getIsAjaxRequest()) { return true; } + $fileData = $value; + + if (is_string($value) && !empty($value)) { + $dir = $this->_directory->getAbsolutePath($this->getAttribute()->getEntityType()->getEntityTypeCode()); + $fileData = [ + 'size' => filesize($dir . $value), + 'name' => $value, + 'tmp_name' => $dir . $value + ]; + } $errors = []; $attribute = $this->getAttribute(); $toDelete = !empty($value['delete']) ? true : false; - $toUpload = !empty($value['tmp_name']) ? true : false; + $toUpload = !empty($value['tmp_name']) || is_string($value) && !empty($value) ? true : false; if (!$toUpload && !$toDelete && $this->getEntity()->getData($attribute->getAttributeCode())) { return true; @@ -195,11 +204,13 @@ public function validateValue($value) } if ($toUpload) { - $errors = array_merge($errors, $this->_validateByRules($value)); + $errors = array_merge($errors, $this->_validateByRules($fileData)); } if (count($errors) == 0) { return true; + } elseif (is_string($value) && !empty($value)) { + $this->_directory->delete($dir . $value); } return $errors; diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php index b15174960524c..402497a1379c0 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Eav\Test\Unit\Model\Entity; +/** + * Class AttributeTest. + */ class AttributeTest extends \PHPUnit\Framework\TestCase { /** @@ -128,6 +131,7 @@ public function testGetFrontendLabels() $attributeId = 1; $storeLabels = ['test_attribute_store1']; $frontendLabelFactory = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\FrontendLabelFactory::class) + ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); $resource = $this->getMockBuilder(\Magento\Eav\Model\ResourceModel\Entity\Attribute::class) diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index 204e9b11085ed..a0992e28bb2cd 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -13,6 +13,10 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; +/** + * Import validate controller action. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Validate extends ImportResultController implements HttpPostActionInterface { /** @@ -24,6 +28,7 @@ class Validate extends ImportResultController implements HttpPostActionInterface * Validate uploaded files action * * @return \Magento\Framework\Controller\ResultInterface + * @SuppressWarnings(PHPMD.Superglobals) */ public function execute() { @@ -42,12 +47,7 @@ public function execute() /** @var $import \Magento\ImportExport\Model\Import */ $import = $this->getImport()->setData($data); try { - $source = ImportAdapter::findAdapterFor( - $import->uploadSource(), - $this->_objectManager->create(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::ROOT), - $data[$import::FIELD_FIELD_SEPARATOR] - ); + $source = $import->uploadFileAndGetSource(); $this->processValidationResult($import->validateSource($source), $resultBlock); } catch (\Magento\Framework\Exception\LocalizedException $e) { $resultBlock->addError($e->getMessage()); @@ -72,6 +72,7 @@ public function execute() * @param bool $validationResult * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function processValidationResult($validationResult, $resultBlock) { @@ -109,6 +110,8 @@ private function processValidationResult($validationResult, $resultBlock) } /** + * Provides import model. + * * @return Import * @deprecated 100.1.0 */ @@ -128,6 +131,7 @@ private function getImport() * * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function addMessageToSkipErrors(Result $resultBlock) { @@ -148,6 +152,7 @@ private function addMessageToSkipErrors(Result $resultBlock) * * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function addMessageForValidResult(Result $resultBlock) { @@ -166,6 +171,7 @@ private function addMessageForValidResult(Result $resultBlock) * * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function collectErrors(Result $resultBlock) { diff --git a/app/code/Magento/ImportExport/Model/History.php b/app/code/Magento/ImportExport/Model/History.php index 3138f150d5fc5..b85bf7da81a35 100644 --- a/app/code/Magento/ImportExport/Model/History.php +++ b/app/code/Magento/ImportExport/Model/History.php @@ -42,6 +42,11 @@ class History extends \Magento\Framework\Model\AbstractModel */ protected $reportHelper; + /** + * @var \Magento\Backend\Model\Auth\Session + */ + protected $session; + /** * Class constructor * @@ -293,6 +298,8 @@ public function setSummary($summary) } /** + * Load the last inserted item + * * @return $this */ public function loadLastInsertItem() diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 064c696ad0a84..1372322ee5855 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; @@ -19,6 +20,7 @@ * @method string getBehavior() getBehavior() * @method \Magento\ImportExport\Model\Import setEntity() setEntity(string $value) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ class Import extends \Magento\ImportExport\Model\AbstractModel @@ -167,6 +169,16 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ protected $_filesystem; + /** + * @var History + */ + private $importHistoryModel; + + /** + * @var DateTime + */ + private $localeDate; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Filesystem $filesystem @@ -181,7 +193,7 @@ class Import extends \Magento\ImportExport\Model\AbstractModel * @param Source\Import\Behavior\Factory $behaviorFactory * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry * @param History $importHistoryModel - * @param \Magento\Framework\Stdlib\DateTime\DateTime $localeDate + * @param DateTime $localeDate * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -199,7 +211,7 @@ public function __construct( \Magento\ImportExport\Model\Source\Import\Behavior\Factory $behaviorFactory, \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\ImportExport\Model\History $importHistoryModel, - \Magento\Framework\Stdlib\DateTime\DateTime $localeDate, + DateTime $localeDate, array $data = [] ) { $this->_importExportData = $importExportData; @@ -268,6 +280,7 @@ protected function _getEntityAdapter() * * @param string $sourceFile Full path to source file * @return \Magento\ImportExport\Model\Import\AbstractSource + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _getSourceAdapter($sourceFile) { @@ -283,6 +296,7 @@ protected function _getSourceAdapter($sourceFile) * * @param ProcessingErrorAggregatorInterface $validationResult * @return string[] + * @throws \Magento\Framework\Exception\LocalizedException */ public function getOperationResultMessages(ProcessingErrorAggregatorInterface $validationResult) { @@ -379,6 +393,7 @@ public function getEntity() * Returns number of checked entities. * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getProcessedEntitiesCount() { @@ -389,6 +404,7 @@ public function getProcessedEntitiesCount() * Returns number of checked rows. * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getProcessedRowsCount() { @@ -443,7 +459,7 @@ public function importSource() } /** - * Processing of import. + * Process import. * * @return bool * @throws \Magento\Framework\Exception\LocalizedException @@ -457,6 +473,7 @@ protected function processImport() * Import possibility getter. * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function isImportAllowed() { @@ -475,7 +492,7 @@ public function getErrorAggregator() } /** - * Move uploaded file and create source adapter instance. + * Move uploaded file. * * @throws \Magento\Framework\Exception\LocalizedException * @return string Source file path @@ -527,14 +544,27 @@ public function uploadSource() } $this->_removeBom($sourceFile); $this->createHistoryReport($sourceFileRelative, $entity, $extension, $result); - // trying to create source adapter for file and catch possible exception to be convinced in its adequacy + return $sourceFile; + } + + /** + * Move uploaded file and provide source instance. + * + * @return Import\AbstractSource + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function uploadFileAndGetSource() + { + $sourceFile = $this->uploadSource(); try { - $this->_getSourceAdapter($sourceFile); + $source = $this->_getSourceAdapter($sourceFile); } catch (\Exception $e) { - $this->_varDirectory->delete($sourceFileRelative); + $this->_varDirectory->delete($this->_varDirectory->getRelativePath($sourceFile)); throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage())); } - return $sourceFile; + + return $source; } /** @@ -542,6 +572,7 @@ public function uploadSource() * * @param string $sourceFile * @return $this + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _removeBom($sourceFile) { @@ -561,6 +592,7 @@ protected function _removeBom($sourceFile) * * @param \Magento\ImportExport\Model\Import\AbstractSource $source * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $source) { @@ -604,6 +636,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource * Invalidate indexes by process codes. * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function invalidateIndex() { @@ -670,6 +703,7 @@ public function getEntityBehaviors() * ) * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getUniqueEntityBehaviors() { @@ -760,6 +794,7 @@ protected function createHistoryReport($sourceFileRelative, $entity, $extension * Get count of created items * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getCreatedItemsCount() { @@ -770,6 +805,7 @@ public function getCreatedItemsCount() * Get count of updated items * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getUpdatedItemsCount() { @@ -780,6 +816,7 @@ public function getUpdatedItemsCount() * Get count of deleted items * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getDeletedItemsCount() { diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php index b7fafc43ca4ed..6fa87ab5d5c4d 100644 --- a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php +++ b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model\Import\Source; +use Magento\Framework\Exception\ValidatorException; + /** * Zip import adapter. */ @@ -14,6 +16,8 @@ class Zip extends Csv * @param string $file * @param \Magento\Framework\Filesystem\Directory\Write $directory * @param string $options + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\ValidatorException */ public function __construct( $file, @@ -21,10 +25,14 @@ public function __construct( $options ) { $zip = new \Magento\Framework\Archive\Zip(); - $file = $zip->unpack( - $directory->getRelativePath($file), - $directory->getRelativePath(preg_replace('/\.zip$/i', '.csv', $file)) + $csvFile = $zip->unpack( + $file, + preg_replace('/\.zip$/i', '.csv', $file) ); - parent::__construct($file, $directory, $options); + if (!$csvFile) { + throw new ValidatorException(__('Sorry, but the data is invalid or the file is not uploaded.')); + } + $directory->delete($directory->getRelativePath($file)); + parent::__construct($csvFile, $directory, $options); } } diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php index 889fc11d71d7e..8ee6de1e44476 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php @@ -17,6 +17,9 @@ use Magento\Newsletter\Model\Subscriber; use Magento\Newsletter\Model\SubscriberFactory; +/** + * Class ProblemTest + */ class ProblemTest extends \PHPUnit\Framework\TestCase { /** @@ -59,6 +62,9 @@ class ProblemTest extends \PHPUnit\Framework\TestCase */ private $problemModel; + /** + * @inheritdoc + */ protected function setUp() { $this->contextMock = $this->getMockBuilder(Context::class) @@ -68,6 +74,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->subscriberFactoryMock = $this->getMockBuilder(SubscriberFactory::class) + ->disableOriginalConstructor() ->getMock(); $this->subscriberMock = $this->getMockBuilder(Subscriber::class) ->disableOriginalConstructor() @@ -98,6 +105,9 @@ protected function setUp() ); } + /** + * @return void + */ public function testAddSubscriberData() { $subscriberId = 1; @@ -111,6 +121,9 @@ public function testAddSubscriberData() self::assertEquals($subscriberId, $this->problemModel->getSubscriberId()); } + /** + * @return void + */ public function testAddQueueData() { $queueId = 1; @@ -127,6 +140,9 @@ public function testAddQueueData() self::assertEquals($queueId, $this->problemModel->getQueueId()); } + /** + * @return void + */ public function testAddErrorData() { $exceptionMessage = 'Some message'; @@ -140,17 +156,26 @@ public function testAddErrorData() self::assertEquals($exceptionCode, $this->problemModel->getProblemErrorCode()); } + /** + * @return void + */ public function testGetSubscriberWithNoSubscriberId() { self::assertNull($this->problemModel->getSubscriber()); } + /** + * @return void + */ public function testGetSubscriber() { $this->setSubscriber(); self::assertEquals($this->subscriberMock, $this->problemModel->getSubscriber()); } + /** + * @return void + */ public function testUnsubscribeWithNoSubscriber() { $this->subscriberMock->expects($this->never()) @@ -162,6 +187,9 @@ public function testUnsubscribeWithNoSubscriber() self::assertEquals($this->problemModel, $result); } + /** + * @return void + */ public function testUnsubscribe() { $this->setSubscriber(); diff --git a/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php b/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php index dab8d7be16dd7..d9d26d0848c24 100644 --- a/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php +++ b/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php @@ -6,6 +6,10 @@ namespace Magento\PageCache\Model\System\Config\Backend; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\App\Config\ScopeConfigInterface; + /** * Backend model for processing Public content cache lifetime settings * @@ -13,6 +17,36 @@ */ class Ttl extends \Magento\Framework\App\Config\Value { + /** + * @var Escaper + */ + private $escaper; + + /** + * Ttl constructor. + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param ScopeConfigInterface $config + * @param \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data + * @param Escaper|null $escaper + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + ScopeConfigInterface $config, + \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList, + ?\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + ?\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [], + ?Escaper $escaper = null + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + $this->escaper = $escaper ?: ObjectManager::getInstance()->create(Escaper::class); + } + /** * Throw exception if Ttl data is invalid or empty * @@ -24,7 +58,10 @@ public function beforeSave() $value = $this->getValue(); if ($value < 0 || !preg_match('/^[0-9]+$/', $value)) { throw new \Magento\Framework\Exception\LocalizedException( - __('Ttl value "%1" is not valid. Please use only numbers equal or greater than zero.', $value) + __( + 'Ttl value "%1" is not valid. Please use only numbers equal or greater than zero.', + $this->escaper->escapeHtml($value) + ) ); } return $this; diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml index c127371cb5f47..afa71fe591495 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -40,9 +40,7 @@ $params = $block->getParams(); $(parent).trigger('clearTimeout'); fullScreenLoader.stopLoader(); globalMessageList.addErrorMessage({ - message: $t( - 'A server error stopped your order from being placed. Please try to place your order again.' - ) + message: $t() }); } ); diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php index 571d73d07b68e..fa131f9591fa9 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php @@ -7,12 +7,17 @@ use Magento\Checkout\Controller\Express\RedirectLoginInterface; use Magento\Framework\App\Action\Action as AppAction; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Abstract Express Checkout Controller * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -abstract class AbstractExpress extends AppAction implements RedirectLoginInterface +abstract class AbstractExpress extends AppAction implements + RedirectLoginInterface, + HttpGetActionInterface, + HttpPostActionInterface { /** * @var \Magento\Paypal\Model\Express\Checkout @@ -137,6 +142,14 @@ protected function _initCheckout() $this->getResponse()->setStatusHeader(403, '1.1', 'Forbidden'); throw new \Magento\Framework\Exception\LocalizedException(__('We can\'t initialize Express Checkout.')); } + if (!(float)$quote->getGrandTotal()) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'PayPal can\'t process orders with a zero balance due. ' + . 'To finish your purchase, please go through the standard checkout process.' + ) + ); + } if (!isset($this->_checkoutTypes[$this->_checkoutType])) { $parameters = [ 'params' => [ @@ -151,6 +164,8 @@ protected function _initCheckout() } /** + * Get Proper Checkout Token + * * Search for proper checkout token in request or session or (un)set specified one * Combined getter/setter * @@ -221,8 +236,7 @@ protected function _getQuote() } /** - * Returns before_auth_url redirect parameter for customer session - * @return null + * @inheritdoc */ public function getCustomerBeforeAuthUrl() { @@ -230,8 +244,7 @@ public function getCustomerBeforeAuthUrl() } /** - * Returns a list of action flags [flag_key] => boolean - * @return array + * @inheritdoc */ public function getActionFlagList() { @@ -240,6 +253,7 @@ public function getActionFlagList() /** * Returns login url parameter for redirect + * * @return string */ public function getLoginUrl() @@ -249,6 +263,7 @@ public function getLoginUrl() /** * Returns action name which requires redirect + * * @return string */ public function getRedirectActionName() @@ -269,4 +284,9 @@ public function redirectLogin() $this->_urlHelper->addRequestParam($this->_customerUrl->getLoginUrl(), ['context' => 'checkout']) ); } + + /** + * @inheritdoc + */ + abstract public function execute(); } diff --git a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php index dd4d73930cc8f..65cb537e89fec 100644 --- a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php +++ b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php @@ -1,15 +1,21 @@ ]*href\s*?=\s*?([\"\']??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU"; + $regexp = "#(?J).*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" + .">?(?:(?:(?.*?)(?:<\/a\s*>?|(?=<\w))|(?.*)))#si"; while (preg_match($regexp, $data, $matches)) { - //Revert the sprintf escaping - $url = str_replace('%%', '%', $matches[2]); - $text = str_replace('%%', '%', $matches[3]); - //Check for an valid url - if ($url) { - $urlScheme = strtolower(parse_url($url, PHP_URL_SCHEME)); - if ($urlScheme !== 'http' && $urlScheme !== 'https') { - $url = null; - } - } - //Use hash tag as fallback - if (!$url) { - $url = '#'; + $text = ''; + if (!empty($matches['text'])) { + $text = str_replace('%%', '%', $matches['text']); } + $url = $this->filterUrl($matches['link'] ?? ''); //Recreate a minimalistic secure a tag $links[] = sprintf( '%s', @@ -178,4 +176,29 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) } return $this->escaper->escapeHtml($data, $allowedTags); } + + /** + * Filter the URL for allowed protocols. + * + * @param string $url + * @return string + */ + private function filterUrl(string $url): string + { + if ($url) { + //Revert the sprintf escaping + $url = str_replace('%%', '%', $url); + $urlScheme = parse_url($url, PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { + $url = null; + } + } + + if (!$url) { + $url = '#'; + } + + return $url; + } } diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 345ff036a2be6..09f0e5f60c914 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -13,6 +13,7 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Order\Address\Collection; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection as CreditmemoCollection; use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection as InvoiceCollection; @@ -279,6 +280,11 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ private $localeResolver; + /** + * @var ProductOption + */ + private $productOption; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -308,6 +314,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param ResolverInterface $localeResolver + * @param ProductOption|null $productOption * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -338,7 +345,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - ResolverInterface $localeResolver = null + ResolverInterface $localeResolver = null, + ProductOption $productOption = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -361,6 +369,7 @@ public function __construct( $this->salesOrderCollectionFactory = $salesOrderCollectionFactory; $this->priceCurrency = $priceCurrency; $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(ResolverInterface::class); + $this->productOption = $productOption ?: ObjectManager::getInstance()->get(ProductOption::class); parent::__construct( $context, @@ -547,12 +556,7 @@ public function canCancel() } } - $allRefunded = true; - foreach ($this->getAllItems() as $orderItem) { - $allRefunded = $allRefunded && ((float)$orderItem->getQtyRefunded() == (float)$orderItem->getQtyInvoiced()); - } - - if ($allInvoiced && !$allRefunded) { + if ($allInvoiced) { return false; } @@ -1347,6 +1351,7 @@ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false if ($this->getId()) { foreach ($collection as $item) { $item->setOrder($this); + $this->productOption->add($item); } } return $collection; diff --git a/app/code/Magento/Sales/Model/Order/Item.php b/app/code/Magento/Sales/Model/Order/Item.php index 710c85f8773d6..a0eff45179ac8 100644 --- a/app/code/Magento/Sales/Model/Order/Item.php +++ b/app/code/Magento/Sales/Model/Order/Item.php @@ -232,7 +232,7 @@ public function getQtyToShip() */ public function getSimpleQtyToShip() { - $qty = $this->getQtyOrdered() - $this->getQtyShipped() - $this->getQtyCanceled(); + $qty = $this->getQtyOrdered() - $this->getQtyShipped() - $this->getQtyRefunded() - $this->getQtyCanceled(); return max(round($qty, 8), 0); } diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index cdf5bdf99786e..6e029ac468370 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -6,10 +6,8 @@ namespace Magento\Sales\Model\Order; -use Magento\Catalog\Api\Data\ProductOptionExtensionFactory; -use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Catalog\Model\ProductOptionFactory; use Magento\Catalog\Model\ProductOptionProcessorInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\DataObject; use Magento\Framework\DataObject\Factory as DataObjectFactory; @@ -18,6 +16,7 @@ use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory; use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Metadata; /** @@ -41,16 +40,6 @@ class ItemRepository implements OrderItemRepositoryInterface */ protected $searchResultFactory; - /** - * @var ProductOptionFactory - */ - protected $productOptionFactory; - - /** - * @var ProductOptionExtensionFactory - */ - protected $extensionFactory; - /** * @var ProductOptionProcessorInterface[] */ @@ -62,40 +51,41 @@ class ItemRepository implements OrderItemRepositoryInterface protected $registry = []; /** - * @var \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface + * @var CollectionProcessorInterface */ private $collectionProcessor; /** - * ItemRepository constructor. + * @var ProductOption + */ + private $productOption; + + /** * @param DataObjectFactory $objectFactory * @param Metadata $metadata * @param OrderItemSearchResultInterfaceFactory $searchResultFactory - * @param ProductOptionFactory $productOptionFactory - * @param ProductOptionExtensionFactory $extensionFactory + * @param CollectionProcessorInterface $collectionProcessor + * @param ProductOption $productOption * @param array $processorPool - * @param CollectionProcessorInterface|null $collectionProcessor */ public function __construct( DataObjectFactory $objectFactory, Metadata $metadata, OrderItemSearchResultInterfaceFactory $searchResultFactory, - ProductOptionFactory $productOptionFactory, - ProductOptionExtensionFactory $extensionFactory, - array $processorPool = [], - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor, + ProductOption $productOption, + array $processorPool = [] ) { $this->objectFactory = $objectFactory; $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; - $this->productOptionFactory = $productOptionFactory; - $this->extensionFactory = $extensionFactory; + $this->collectionProcessor = $collectionProcessor; + $this->productOption = $productOption; $this->processorPool = $processorPool; - $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); } /** - * Load entity + * Loads entity. * * @param int $id * @return OrderItemInterface @@ -116,7 +106,7 @@ public function get($id) ); } - $this->addProductOption($orderItem); + $this->productOption->add($orderItem); $this->addParentItem($orderItem); $this->registry[$id] = $orderItem; } @@ -137,7 +127,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) $this->collectionProcessor->process($searchCriteria, $searchResult); /** @var OrderItemInterface $orderItem */ foreach ($searchResult->getItems() as $orderItem) { - $this->addProductOption($orderItem); + $this->productOption->add($orderItem); } return $searchResult; @@ -178,7 +168,9 @@ public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { $request = $this->getBuyRequest($entity); - $entity->setProductOptions(['info_buyRequest' => $request->toArray()]); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $request->toArray(); + $entity->setProductOptions($productOptions); } $this->metadata->getMapper()->save($entity); @@ -186,37 +178,6 @@ public function save(OrderItemInterface $entity) return $this->registry[$entity->getEntityId()]; } - /** - * Add product option data - * - * @param OrderItemInterface $orderItem - * @return $this - */ - protected function addProductOption(OrderItemInterface $orderItem) - { - /** @var DataObject $request */ - $request = $orderItem->getBuyRequest(); - - $productType = $orderItem->getProductType(); - if (isset($this->processorPool[$productType]) - && !$orderItem->getParentItemId()) { - $data = $this->processorPool[$productType]->convertToProductOption($request); - if ($data) { - $this->setProductOption($orderItem, $data); - } - } - - if (isset($this->processorPool['custom_options']) - && !$orderItem->getParentItemId()) { - $data = $this->processorPool['custom_options']->convertToProductOption($request); - if ($data) { - $this->setProductOption($orderItem, $data); - } - } - - return $this; - } - /** * Set parent item. * @@ -239,32 +200,6 @@ private function addParentItem(OrderItemInterface $orderItem) } } - /** - * Set product options data - * - * @param OrderItemInterface $orderItem - * @param array $data - * @return $this - */ - protected function setProductOption(OrderItemInterface $orderItem, array $data) - { - $productOption = $orderItem->getProductOption(); - if (!$productOption) { - $productOption = $this->productOptionFactory->create(); - $orderItem->setProductOption($productOption); - } - - $extensionAttributes = $productOption->getExtensionAttributes(); - if (!$extensionAttributes) { - $extensionAttributes = $this->extensionFactory->create(); - $productOption->setExtensionAttributes($extensionAttributes); - } - - $extensionAttributes->setData(key($data), current($data)); - - return $this; - } - /** * Retrieve order item's buy request * @@ -296,20 +231,4 @@ protected function getBuyRequest(OrderItemInterface $entity) return $request; } - - /** - * Retrieve collection processor - * - * @deprecated 100.2.0 - * @return CollectionProcessorInterface - */ - private function getCollectionProcessor() - { - if (!$this->collectionProcessor) { - $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - } - return $this->collectionProcessor; - } } diff --git a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 85e34f560bb7b..8cdc90972bbb0 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -363,6 +363,38 @@ protected function _calcAddressHeight($address) return $y; } + /** + * Detect an input string is Arabic + * + * @param string $subject + * @return bool + */ + private function isArabic(string $subject): bool + { + return (preg_match('/\p{Arabic}/u', $subject) > 0); + } + + /** + * Reverse text with Arabic characters + * + * @param string $string + * @return string + */ + private function reverseArabicText($string) + { + $splitText = explode(' ', $string); + for ($i = 0; $i < count($splitText); $i++) { + if ($this->isArabic($splitText[$i])) { + for ($j = $i + 1; $j < count($splitText); $j++) { + $tmp = $this->string->strrev($splitText[$j]); + $splitText[$j] = $this->string->strrev($splitText[$i]); + $splitText[$i] = $tmp; + } + } + } + return implode(' ', $splitText); + } + /** * Insert order to pdf page * @@ -474,7 +506,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if ($value !== '') { $text = []; foreach ($this->string->split($value, 45, true, true) as $_value) { - $text[] = $_value; + $text[] = ($this->isArabic($_value)) ? $this->reverseArabicText($_value) : $_value; } foreach ($text as $part) { $page->drawText(strip_tags(ltrim($part)), 35, $this->y, 'UTF-8'); @@ -491,7 +523,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if ($value !== '') { $text = []; foreach ($this->string->split($value, 45, true, true) as $_value) { - $text[] = $_value; + $text[] = ($this->isArabic($_value)) ? $this->reverseArabicText($_value) : $_value; } foreach ($text as $part) { $page->drawText(strip_tags(ltrim($part)), 285, $this->y, 'UTF-8'); diff --git a/app/code/Magento/Sales/Model/Order/ProductOption.php b/app/code/Magento/Sales/Model/Order/ProductOption.php new file mode 100644 index 0000000000000..dc9ec42e27e60 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/ProductOption.php @@ -0,0 +1,103 @@ +productOptionFactory = $productOptionFactory; + $this->extensionFactory = $extensionFactory; + $this->processorPool = $processorPool; + } + + /** + * Adds product option to the order item. + * + * @param OrderItemInterface $orderItem + */ + public function add(OrderItemInterface $orderItem): void + { + /** @var DataObject $request */ + $request = $orderItem->getBuyRequest(); + + $productType = $orderItem->getProductType(); + if (isset($this->processorPool[$productType]) + && !$orderItem->getParentItemId()) { + $data = $this->processorPool[$productType]->convertToProductOption($request); + if ($data) { + $this->setProductOption($orderItem, $data); + } + } + + if (isset($this->processorPool['custom_options']) + && !$orderItem->getParentItemId()) { + $data = $this->processorPool['custom_options']->convertToProductOption($request); + if ($data) { + $this->setProductOption($orderItem, $data); + } + } + } + + /** + * Sets product options data. + * + * @param OrderItemInterface $orderItem + * @param array $data + */ + private function setProductOption(OrderItemInterface $orderItem, array $data): void + { + $productOption = $orderItem->getProductOption(); + if (!$productOption) { + $productOption = $this->productOptionFactory->create(); + $orderItem->setProductOption($productOption); + } + + $extensionAttributes = $productOption->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->extensionFactory->create(); + $productOption->setExtensionAttributes($extensionAttributes); + } + + $extensionAttributes->setData(key($data), current($data)); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index ebedc869e14bd..cecee4283648d 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -354,7 +354,15 @@ public function addItem(\Magento\Sales\Model\Order\Shipment\Item $item) public function getTracksCollection() { if ($this->tracksCollection === null) { - $this->tracksCollection = $this->_trackCollectionFactory->create()->setShipmentFilter($this->getId()); + $this->tracksCollection = $this->_trackCollectionFactory->create(); + + if ($this->getId()) { + $this->tracksCollection->setShipmentFilter($this->getId()); + + foreach ($this->tracksCollection as $item) { + $item->setShipment($this); + } + } } return $this->tracksCollection; @@ -400,19 +408,20 @@ public function getTrackById($trackId) */ public function addTrack(\Magento\Sales\Model\Order\Shipment\Track $track) { - $track->setShipment( - $this - )->setParentId( - $this->getId() - )->setOrderId( - $this->getOrderId() - )->setStoreId( - $this->getStoreId() - ); + $track->setShipment($this) + ->setParentId($this->getId()) + ->setOrderId($this->getOrderId()) + ->setStoreId($this->getStoreId()); + if (!$track->getId()) { $this->getTracksCollection()->addItem($track); } + $tracks = $this->getTracks(); + // as it's a new track entity, the collection doesn't contain it + $tracks[] = $track; + $this->setTracks($tracks); + /** * Track saving is implemented in _afterSave() * This enforces \Magento\Framework\Model\AbstractModel::save() not to skip _afterSave() @@ -582,14 +591,15 @@ public function setItems($items) /** * Returns tracks * - * @return \Magento\Sales\Api\Data\ShipmentTrackInterface[] + * @return \Magento\Sales\Api\Data\ShipmentTrackInterface[]|null */ public function getTracks() { + if (!$this->getId()) { + return $this->getData(ShipmentInterface::TRACKS); + } + if ($this->getData(ShipmentInterface::TRACKS) === null) { - foreach ($this->getTracksCollection() as $item) { - $item->setShipment($this); - } $this->setData(ShipmentInterface::TRACKS, $this->getTracksCollection()->getItems()); } return $this->getData(ShipmentInterface::TRACKS); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php index 9c8671d02c578..5851b2d936139 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php @@ -62,8 +62,8 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $object) $this->shipmentItemResource->save($item); } } - if (null !== $object->getTracksCollection()) { - foreach ($object->getTracksCollection() as $track) { + if (null !== $object->getTracks()) { + foreach ($object->getTracks() as $track) { $track->setParentId($object->getId()); $this->shipmentTrackResource->save($track); } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index cc8a62ca48961..654c03bfe9ff4 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -94,24 +94,6 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml deleted file mode 100644 index e4fd894f608c5..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - <description value="Admin should be able to ship remaining ordered items if some of them are already refunded"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-93030"/> - <group value="sales"/> - - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="_defaultProduct" stepKey="createSimpleProduct"> - <requiredEntity createDataKey="createCategory"/> - </createData> - </before> - <after> - <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - </after> - - <!--login to Admin--> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> - - <!--Create new order--> - <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> - <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> - <actionGroup ref="addSimpleProductToOrderWithCustomQuantity" stepKey="addSimpleProductToOrderWithUserDefinedQty"> - <argument name="product" value="_defaultProduct"/> - <argument name="quantity" value="10"/> - </actionGroup> - - <!--Fill customer group and customer email which both are now required fields--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSimpleProductToOrderWithUserDefinedQty"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> - - <!--Fill customer address information--> - <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="fillCustomerEmail"> - <argument name="customer" value="Simple_US_Customer"/> - <argument name="address" value="US_Address_TX"/> - </actionGroup> - <!-- Select shipping --> - <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" after="fillCustomerAddress"/> - - <!--Verify totals on Order page--> - <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="$1,230.00" stepKey="seeOrderSubTotal" after="selectFlatRateShipping"/> - <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="$50.00" stepKey="seeOrderShipping" after="seeOrderSubTotal"/> - <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> - <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="$1,280.00" stepKey="seeCorrectGrandTotal" after="scrollToOrderGrandTotal"/> - - <!--Submit Order and verify information--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder" after="seeCorrectGrandTotal"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskTodisappear"/> - <waitForPageLoad stepKey="waitForOrderToProcess"/> - <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> - <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId" after="seeSuccessMessage"/> - <scrollTo selector="{{AdminOrderItemsOrderedSection.qtyColumn}}" stepKey="scrollToItemsOrdered" after="getOrderId"/> - <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Ordered 10" stepKey="seeQtyOfItemsOrdered" after="scrollToItemsOrdered"/> - - <!--Create order invoice for first half of ordered items--> - <comment userInput="Admin creates invoice for order" stepKey="adminCreateInvoiceComment" /> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceActionButton"/> - <!--<seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" stepKey="seeOrderInvoiceUrl" after="clickInvoiceAction"/>--> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage" after="clickInvoiceActionButton"/> - <scrollTo selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="scrollToItemsInvoiced"/> - - <!--Verify items invoiced information--> - <fillField selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" userInput="5" stepKey="invoiceHalfTheItems"/> - <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQtyToBeInvoiced"/> - <waitForLoadingMaskToDisappear stepKey="WaitForQtyToUpdate"/> - <waitForElementVisible selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="waitforSubmitInvoiceBtn"/> - - <!--Submit Invoice--> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="submitInvoice"/> - <waitForLoadingMaskToDisappear stepKey="WaitForInvoiceToSubmit"/> - <!--<waitForElementVisible selector="{{AdminOrderDetailsMessagesSection.successMessage}}" stepKey="waitUntilInvoiceSucesssMsg"/>--> - - <!--Invoice created successfully--> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceSuccessMessage"/> - <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoicesTab"/> - <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridToLoad" after="clickInvoicesTab"/> - <see selector="{{AdminOrderInvoicesTabSection.gridRow('1')}}" userInput="$665.00" stepKey="seeOrderInvoiceTabInGrid" after="waitForInvoiceGridToLoad"/> - <click selector="{{AdminOrderInvoicesTabSection.viewGridRow('1')}}" stepKey="clickToViewInvoice" after="seeOrderInvoiceTabInGrid"/> - <click selector="{{AdminInvoiceOrderInformationSection.orderId}}" stepKey="clickOrderIdLinkOnInvoice"/> - - <!--Ship Order--> - <comment userInput="Admin creates shipment" stepKey="adminCreatesShipmentComment" before="clickShip"/> - <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShip" after="clickOrderIdLinkOnInvoice"/> - <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="OrderShipmentUrl" after="clickShip"/> - <scrollTo selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" stepKey="scrollToItemsToShip"/> - <see selector="{{AdminShipmentItemsSection.itemQty('1')}}" userInput="Invoiced 5" stepKey="see5itemsInvoiced"/> - <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="5" stepKey="fillQtyOfItemsToShip"/> - - <!--Submit Shipment--> - <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="submitShipment" after="fillQtyOfItemsToShip"/> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="successfullShipmentCreation" after="submitShipment"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="$getOrderId" stepKey="seeOrderIdInPageTitleAfterShip"/> - - <!--Verify Items Status and Shipped Qty in the Items Ordered section--> - <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToItemsShipped"/> - <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Shipped 5" stepKey="see5itemsShipped"/> - - <!--Create Credit Memo--> - <comment userInput="Admin creates credit memo" stepKey="createCreditMemoComment"/> - <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreateCreditMemo" after="createCreditMemoComment"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle"/> - - <!--Submit refund--> - <scrollTo selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" stepKey="scrollToItemsToRefund"/> - <!--<fillField selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" userInput="5" stepKey="fillQtyOfItemsToRefund" after="scrollToItemsToRefund"/>--> - <!--<click selector="{{AdminCreditMemoItemsSection.updateQty}}" stepKey="updateRefundQty"/>--> - <waitForLoadingMaskToDisappear stepKey="waitForRefundQtyToUpdate"/> - <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="seeSubmitRefundBtn"/> - <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="submitRefundOffline"/> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the credit memo." stepKey="seeCreditMemoSuccessMsg" after="submitRefundOffline"/> - - <!--Create invoice for rest of the ordered items--> - <comment userInput="Admin creates invoice for rest of the items" stepKey="adminCreateInvoiceComment2" /> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceActionForRestOfItems"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage2" after="clickInvoiceActionForRestOfItems"/> - - <comment userInput="Qty To Invoice is 5" stepKey="seeRemainderInQtyToInvoice"/> - <scrollTo selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="scrollToItemsInvoiced2"/> - <!--<see selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="scrollToItemsInvoiced2"/>--> - <seeInField userInput="5" selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="see5InTheQtyToInvoice"/> - - <!--Verify items invoiced information--> - <see selector="{{AdminInvoiceItemsSection.itemQty('1')}}" userInput="Refunded 5" stepKey="seeQtyOfItemsRefunded"/> - - <!--Submit Invoice--> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="submitInvoice2" /> - - <!--Invoice created successfully for the rest of the ordered items--> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceSuccessMessage2" after="submitInvoice2"/> - - <!--Verify Ship Action can be done for the rest of the invoiced items --> - <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipActionForRestOfItems" after="seeInvoiceSuccessMessage2"/> - - <!--Verify Items To Ship section --> - <scrollTo selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" stepKey="scrollToItemsToShip2"/> - <see selector="{{AdminShipmentItemsSection.itemQty('1')}}" userInput="Invoiced 10" stepKey="SeeAll10ItemsInvoiced"/> - <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="5" stepKey="fillRestOfItemsToShip"/> - - <!--Submit Shipment--> - <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="submitShipment2" after="fillRestOfItemsToShip"/> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="successfullyCreatedShipment" after="submitShipment2"/> - - <!--Verify Items Status and Shipped Qty in the Items Ordered section--> - <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToItemsOrdered2"/> - <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Shipped 10" stepKey="seeAllItemsShipped"/> - </test> -</tests> - diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php deleted file mode 100644 index 7f3626c25d8df..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php +++ /dev/null @@ -1,409 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Model\Order; - -use Magento\Sales\Model\Order\ItemRepository; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ItemRepositoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\DataObject\Factory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectFactory; - - /** - * @var \Magento\Sales\Model\ResourceModel\Metadata|\PHPUnit_Framework_MockObject_MockObject - */ - protected $metadata; - - /** - * @var \Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $searchResultFactory; - - /** - * @var \Magento\Catalog\Model\ProductOptionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productOptionProcessorMock; - - /** - * @var \Magento\Catalog\Model\ProductOptionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productOptionFactory; - - /** - * @var \Magento\Catalog\Api\Data\ProductOptionExtensionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $extensionFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $collectionProcessor; - - /** - * @var array - */ - protected $productOptionData = []; - - protected function setUp() - { - $this->objectFactory = $this->getMockBuilder(\Magento\Framework\DataObject\Factory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->metadata = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Metadata::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->searchResultFactory = $this->getMockBuilder( - \Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->productOptionFactory = $this->getMockBuilder(\Magento\Catalog\Model\ProductOptionFactory::class) - ->setMethods([ - 'create', - ]) - ->disableOriginalConstructor() - ->getMock(); - - $this->collectionProcessor = $this->createMock( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - - $this->extensionFactory = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductOptionExtensionFactory::class) - ->setMethods([ - 'create', - ]) - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage An ID is needed. Set the ID and try again. - */ - public function testGetWithNoId() - { - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [], - $this->collectionProcessor - ); - - $model->get(null); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage The entity that was requested doesn't exist. Verify the entity and try again. - */ - public function testGetEmptyEntity() - { - $orderItemId = 1; - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn(null); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [], - $this->collectionProcessor - ); - - $model->get($orderItemId); - } - - public function testGetAsParentWithChild() - { - $orderItemId = 1; - $productType = 'configurable'; - - $this->productOptionData = ['option1' => 'value1']; - - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderItemMock($productType, $productOption); - - $orderItemCollectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Item\Collection::class); - $orderItemCollectionMock->expects($this->once()) - ->method('filterByParent') - ->with($orderItemId) - ->willReturnSelf(); - $orderItemCollectionMock->expects($this->once()) - ->method('getItems') - ->willReturn([$orderItemMock]); - - $orderMock = $this->createMock(\Magento\Sales\Model\Order::class); - $orderMock->expects($this->once()) - ->method('getItemsCollection') - ->willReturn($orderItemCollectionMock); - - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->exactly(3)) - ->method('getItemId') - ->willReturn($orderItemId); - $orderItemMock->expects($this->once()) - ->method('getOrder') - ->willReturn($orderMock); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertSame($orderItemMock, $model->get($orderItemId)); - - // Assert already registered - $this->assertSame($orderItemMock, $model->get($orderItemId)); - } - - public function testGetAsChild() - { - $orderItemId = 1; - $parentItemId = 66; - $productType = 'configurable'; - - $this->productOptionData = ['option1' => 'value1']; - - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderItemMock($productType, $productOption); - - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn($orderItemId); - $orderItemMock->expects($this->exactly(3)) - ->method('getParentItemId') - ->willReturn($parentItemId); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $parentItemMock = $this->createMock(\Magento\Sales\Model\Order\Item::class); - - $model = $this->getModel($orderItemMock, $productType); - $reflectedRegistryProperty = new \ReflectionProperty($model, 'registry'); - $reflectedRegistryProperty->setAccessible(true); - $reflectedRegistryProperty->setValue($model, [$parentItemId => $parentItemMock]); - $this->assertSame($orderItemMock, $model->get($orderItemId)); - - // Assert already registered - $this->assertSame($orderItemMock, $model->get($orderItemId)); - } - - public function testGetList() - { - $productType = 'configurable'; - $this->productOptionData = ['option1' => 'value1']; - $searchCriteriaMock = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteria::class) - ->disableOriginalConstructor() - ->getMock(); - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderItemMock($productType, $productOption); - - $searchResultMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Item\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $searchResultMock->expects($this->once()) - ->method('getItems') - ->willReturn([$orderItemMock]); - - $this->searchResultFactory->expects($this->once()) - ->method('create') - ->willReturn($searchResultMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertSame($searchResultMock, $model->getList($searchCriteriaMock)); - } - - public function testDeleteById() - { - $orderItemId = 1; - $productType = 'configurable'; - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('getEntityId') - ->willReturn($orderItemId); - - $orderItemResourceMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemResourceMock->expects($this->once()) - ->method('delete') - ->with($orderItemMock) - ->willReturnSelf(); - - $this->metadata->expects($this->exactly(1)) - ->method('getMapper') - ->willReturn($orderItemResourceMock); - - $model = $this->getModel($orderItemMock, $productType); - $reflectedRegistryProperty = new \ReflectionProperty($model, 'registry'); - $reflectedRegistryProperty->setAccessible(true); - $reflectedRegistryProperty->setValue($model, [$orderItemId => $orderItemMock]); - $this->assertTrue($model->deleteById($orderItemId)); - $this->assertEmpty($reflectedRegistryProperty->getValue($model)); - } - - /** - * @param \PHPUnit_Framework_MockObject_MockObject $orderItemMock - * @param string $productType - * @param array $data - * @return ItemRepository - */ - protected function getModel( - \PHPUnit_Framework_MockObject_MockObject $orderItemMock, - $productType, - array $data = [] - ) { - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $requestUpdateMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - $requestUpdateMock->expects($this->any()) - ->method('getData') - ->willReturn($data); - - $this->productOptionProcessorMock = $this->getMockBuilder( - \Magento\Catalog\Model\ProductOptionProcessorInterface::class - ) - ->getMockForAbstractClass(); - $this->productOptionProcessorMock->expects($this->any()) - ->method('convertToProductOption') - ->with($requestMock) - ->willReturn($this->productOptionData); - $this->productOptionProcessorMock->expects($this->any()) - ->method('convertToBuyRequest') - ->with($orderItemMock) - ->willReturn($requestUpdateMock); - - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [ - $productType => $this->productOptionProcessorMock, - 'custom_options' => $this->productOptionProcessorMock - ], - $this->collectionProcessor - ); - return $model; - } - - /** - * @param string $productType - * @param \PHPUnit_Framework_MockObject_MockObject $productOption - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getOrderItemMock($productType, $productOption) - { - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('getProductType') - ->willReturn($productType); - $orderItemMock->expects($this->once()) - ->method('getBuyRequest') - ->willReturn($requestMock); - $orderItemMock->expects($this->any()) - ->method('getProductOption') - ->willReturn(null); - $orderItemMock->expects($this->any()) - ->method('setProductOption') - ->with($productOption) - ->willReturnSelf(); - - return $orderItemMock; - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getProductOptionMock() - { - $productOption = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductOptionInterface::class) - ->getMockForAbstractClass(); - $productOption->expects($this->any()) - ->method('getExtensionAttributes') - ->willReturn(null); - - $this->productOptionFactory->expects($this->any()) - ->method('create') - ->willReturn($productOption); - - return $productOption; - } - - protected function getProductOptionExtensionMock() - { - $productOptionExtension = $this->getMockBuilder( - \Magento\Catalog\Api\Data\ProductOptionExtensionInterface::class - ) - ->setMethods([ - 'setData', - ]) - ->getMockForAbstractClass(); - $productOptionExtension->expects($this->any()) - ->method('setData') - ->with(key($this->productOptionData), current($this->productOptionData)) - ->willReturnSelf(); - - $this->extensionFactory->expects($this->any()) - ->method('create') - ->willReturn($productOptionExtension); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php index 3d4a5785d6254..76bfd62a7b889 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php @@ -295,14 +295,14 @@ public function getItemQtyVariants() 'qty_ordered' => 12, 'qty_invoiced' => 5, 'qty_refunded' => 5, 'qty_shipped' => 0, 'qty_canceled' => 0, ], - 'expectedResult' => ['to_ship' => 12.0, 'to_invoice' => 7.0] + 'expectedResult' => ['to_ship' => 7.0, 'to_invoice' => 7.0] ], 'partially_refunded' => [ 'options' => [ 'qty_ordered' => 12, 'qty_invoiced' => 12, 'qty_refunded' => 5, 'qty_shipped' => 0, 'qty_canceled' => 0, ], - 'expectedResult' => ['to_ship' => 12.0, 'to_invoice' => 0.0] + 'expectedResult' => ['to_ship' => 7.0, 'to_invoice' => 0.0] ], 'partially_shipped' => [ 'options' => [ @@ -316,7 +316,7 @@ public function getItemQtyVariants() 'qty_ordered' => 12, 'qty_invoiced' => 12, 'qty_refunded' => 5, 'qty_shipped' => 4, 'qty_canceled' => 0 ], - 'expectedResult' => ['to_ship' => 8.0, 'to_invoice' => 0.0] + 'expectedResult' => ['to_ship' => 3.0, 'to_invoice' => 0.0] ], 'complete' => [ 'options' => [ @@ -337,7 +337,7 @@ public function getItemQtyVariants() 'qty_ordered' => 4.4, 'qty_invoiced' => 0.4, 'qty_refunded' => 0.4, 'qty_shipped' => 4, 'qty_canceled' => 0, ], - 'expectedResult' => ['to_ship' => 0.4, 'to_invoice' => 4.0] + 'expectedResult' => ['to_ship' => 0.0, 'to_invoice' => 4.0] ], 'completely_invoiced_using_decimals' => [ 'options' => [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index 49c86b5294f9e..f724136eb5154 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -117,8 +117,6 @@ protected function setUp() 'getQuoteItemId', 'getLockedDoInvoice', 'getProductId', - 'getQtyRefunded', - 'getQtyInvoiced', ]); $this->salesOrderCollectionMock = $this->getMockBuilder( \Magento\Sales\Model\ResourceModel\Order\Collection::class @@ -638,163 +636,6 @@ public function testCanCancelAllInvoiced() $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(0); - $this->item->expects($this->any()) - ->method('getQtyRefunded') - ->willReturn(0); - $this->item->expects($this->any()) - ->method('getQtyInvoiced') - ->willReturn(1); - - $this->assertFalse($this->order->canCancel()); - } - - public function testCanCancelAllRefunded() - { - $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) - ->disableOriginalConstructor() - ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) - ->getMock(); - $paymentMock->expects($this->any()) - ->method('canReviewPayment') - ->will($this->returnValue(false)); - $paymentMock->expects($this->any()) - ->method('canFetchTransactionInfo') - ->will($this->returnValue(false)); - $collectionMock = $this->createPartialMock( - \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, - ['getItems', 'setOrderFilter'] - ); - $this->orderItemCollectionFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($collectionMock)); - $collectionMock->expects($this->any()) - ->method('setOrderFilter') - ->willReturnSelf(); - $this->preparePaymentMock($paymentMock); - - $this->prepareItemMock(0); - - $this->order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD, false); - $this->order->setState(\Magento\Sales\Model\Order::STATE_NEW); - - $this->item->expects($this->any()) - ->method('isDeleted') - ->willReturn(false); - $this->item->expects($this->any()) - ->method('getQtyToInvoice') - ->willReturn(0); - $this->item->expects($this->any()) - ->method('getQtyRefunded') - ->willReturn(10); - $this->item->expects($this->any()) - ->method('getQtyInvoiced') - ->willReturn(10); - - $this->assertTrue($this->order->canCancel()); - } - - /** - * Test that order can be canceled if some items were partially invoiced with certain qty - * and then refunded for this qty. - * Sample: - * - ordered qty = 20 - * - invoiced = 10 - * - refunded = 10 - */ - public function testCanCancelPartiallyInvoicedAndRefunded() - { - $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) - ->disableOriginalConstructor() - ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) - ->getMock(); - $paymentMock->expects($this->any()) - ->method('canReviewPayment') - ->will($this->returnValue(false)); - $paymentMock->expects($this->any()) - ->method('canFetchTransactionInfo') - ->will($this->returnValue(false)); - $collectionMock = $this->createPartialMock( - \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, - ['getItems', 'setOrderFilter'] - ); - $this->orderItemCollectionFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($collectionMock)); - $collectionMock->expects($this->any()) - ->method('setOrderFilter') - ->willReturnSelf(); - $this->preparePaymentMock($paymentMock); - - $this->prepareItemMock(0); - - $this->order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD, false); - $this->order->setState(\Magento\Sales\Model\Order::STATE_NEW); - - $this->item->expects($this->any()) - ->method('isDeleted') - ->willReturn(false); - $this->item->expects($this->any()) - ->method('getQtyToInvoice') - ->willReturn(10); - $this->item->expects($this->any()) - ->method('getQtyRefunded') - ->willReturn(10); - $this->item->expects($this->any()) - ->method('getQtyInvoiced') - ->willReturn(10); - - $this->assertTrue($this->order->canCancel()); - } - - /** - * Test that order CAN NOT be canceled if some items were partially invoiced with certain qty - * and then refunded for less than that qty. - * Sample: - * - ordered qty = 10 - * - invoiced = 10 - * - refunded = 5 - */ - public function testCanCancelPartiallyInvoicedAndNotFullyRefunded() - { - $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) - ->disableOriginalConstructor() - ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) - ->getMock(); - $paymentMock->expects($this->any()) - ->method('canReviewPayment') - ->will($this->returnValue(false)); - $paymentMock->expects($this->any()) - ->method('canFetchTransactionInfo') - ->will($this->returnValue(false)); - $collectionMock = $this->createPartialMock( - \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, - ['getItems', 'setOrderFilter'] - ); - $this->orderItemCollectionFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($collectionMock)); - $collectionMock->expects($this->any()) - ->method('setOrderFilter') - ->willReturnSelf(); - $this->preparePaymentMock($paymentMock); - - $this->prepareItemMock(0); - - $this->order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD, false); - $this->order->setState(\Magento\Sales\Model\Order::STATE_NEW); - - $this->item->expects($this->any()) - ->method('isDeleted') - ->willReturn(false); - $this->item->expects($this->any()) - ->method('getQtyToInvoice') - ->willReturn(0); - $this->item->expects($this->any()) - ->method('getQtyRefunded') - ->willReturn(5); - $this->item->expects($this->any()) - ->method('getQtyInvoiced') - ->willReturn(10); $this->assertFalse($this->order->canCancel()); } @@ -1258,9 +1099,6 @@ public function testGetCreatedAtFormattedUsesCorrectLocale() $this->order->getCreatedAtFormatted(\IntlDateFormatter::SHORT); } - /** - * @return array - */ public function notInvoicingStatesProvider() { return [ @@ -1270,9 +1108,6 @@ public function notInvoicingStatesProvider() ]; } - /** - * @return array - */ public function canNotCreditMemoStatesProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php index a7a615fb0f508..530306d77d3ed 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php @@ -3,145 +3,125 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Test\Unit\Model\ResourceModel\Order\Shipment; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Comment as CommentEntity; +use Magento\Sales\Model\Order\Shipment\Item as ItemEntity; +use Magento\Sales\Model\Order\Shipment\Track as TrackEntity; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Comment; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Item; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Relation; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Track; +use PHPUnit\Framework\MockObject\MockObject; + /** * Class RelationTest */ class RelationTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Relation + * @var Relation */ - protected $relationProcessor; + private $relationProcessor; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Item|\PHPUnit_Framework_MockObject_MockObject + * @var Item|MockObject */ - protected $itemResourceMock; + private $itemResource; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Track|\PHPUnit_Framework_MockObject_MockObject + * @var Track|MockObject */ - protected $trackResourceMock; + private $trackResource; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment|\PHPUnit_Framework_MockObject_MockObject + * @var Comment|MockObject */ - protected $commentResourceMock; + private $commentResource; /** - * @var \Magento\Sales\Model\Order\Shipment\Comment|\PHPUnit_Framework_MockObject_MockObject + * @var CommentEntity|MockObject */ - protected $commentMock; + private $comment; /** - * @var \Magento\Sales\Model\Order\Shipment\Track|\PHPUnit_Framework_MockObject_MockObject + * @var TrackEntity|MockObject */ - protected $trackMock; + private $track; /** - * @var \Magento\Sales\Model\Order\Shipment|\PHPUnit_Framework_MockObject_MockObject + * @var Shipment|MockObject */ - protected $shipmentMock; + private $shipment; /** - * @var \Magento\Sales\Model\Order\Shipment\Item|\PHPUnit_Framework_MockObject_MockObject + * @var ItemEntity|MockObject */ - protected $itemMock; + private $item; - protected function setUp() + /** + * @inheritdoc + */ + protected function setUp(): void { - $this->itemResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Item::class) + $this->itemResource = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->commentResourceMock = $this->getMockBuilder( - \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment::class - ) + $this->commentResource = $this->getMockBuilder(Comment::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->trackResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Track::class) + $this->trackResource = $this->getMockBuilder(Track::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->shipmentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + $this->shipment = $this->getMockBuilder(Shipment::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'getId', - 'getItems', - 'getTracks', - 'getComments', - 'getTracksCollection', - ] - ) ->getMock(); - $this->itemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + $this->item = $this->getMockBuilder(ItemEntity::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'setParentId' - ] - ) ->getMock(); - $this->trackMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + $this->track = $this->getMockBuilder(TrackEntity::class) ->disableOriginalConstructor() ->getMock(); - $this->commentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + $this->comment = $this->getMockBuilder(Shipment::class) ->disableOriginalConstructor() ->getMock(); - $this->relationProcessor = new \Magento\Sales\Model\ResourceModel\Order\Shipment\Relation( - $this->itemResourceMock, - $this->trackResourceMock, - $this->commentResourceMock + $this->relationProcessor = new Relation( + $this->itemResource, + $this->trackResource, + $this->commentResource ); } - public function testProcessRelations() + /** + * Checks saving shipment relations. + * + * @throws \Exception + */ + public function testProcessRelations(): void { - $this->shipmentMock->expects($this->exactly(3)) - ->method('getId') + $this->shipment->method('getId') ->willReturn('shipment-id-value'); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getItems') - ->willReturn([$this->itemMock]); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getComments') - ->willReturn([$this->commentMock]); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getTracksCollection') - ->willReturn([$this->trackMock]); - $this->itemMock->expects($this->once()) - ->method('setParentId') + $this->shipment->method('getItems') + ->willReturn([$this->item]); + $this->shipment->method('getComments') + ->willReturn([$this->comment]); + $this->shipment->method('getTracks') + ->willReturn([$this->track]); + $this->item->method('setParentId') ->with('shipment-id-value') ->willReturnSelf(); - $this->itemResourceMock->expects($this->once()) - ->method('save') - ->with($this->itemMock) + $this->itemResource->method('save') + ->with($this->item) ->willReturnSelf(); - $this->commentResourceMock->expects($this->once()) - ->method('save') - ->with($this->commentMock) + $this->commentResource->method('save') + ->with($this->comment) ->willReturnSelf(); - $this->trackResourceMock->expects($this->once()) - ->method('save') - ->with($this->trackMock) + $this->trackResource->method('save') + ->with($this->track) ->willReturnSelf(); - $this->relationProcessor->processRelation($this->shipmentMock); + $this->relationProcessor->processRelation($this->shipment); } } diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index d69fcad7aac61..b9e7ac1c6aca0 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -130,6 +130,8 @@ <html>html</html> <phtml>phtml</phtml> <shtml>shtml</shtml> + <phpt>phpt</phpt> + <pht>pht</pht> </protected_extensions> <public_files_valid_paths> <protected> diff --git a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml index b20da68734579..26ef4847a1267 100644 --- a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml +++ b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml @@ -58,7 +58,7 @@ $schemaUrl = $block->getSchemaUrl(); <div class="swagger-ui-wrap"> <a id="logo" href="http://swagger.io">swagger</a> <form id='api_selector'> - <input id="input_baseUrl" type="hidden" value="<?= /* @escapeNotVerified */ $schemaUrl ?>"/> + <input id="input_baseUrl" type="hidden" value="<?= $block->escapeUrl($schemaUrl) ?>"/> <div class='input'><input placeholder="api_key" id="input_apiKey" name="apiKey" type="text"/></div> <div class='input'><a id="explore" href="#" data-sw-translate>apply</a></div> </form> diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 63dbd31751e85..7ec7eab23283a 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -501,7 +501,9 @@ define([ label, width, height, - attr; + attr, + swatchImageWidth, + swatchImageHeight; if (!optionConfig.hasOwnProperty(this.id)) { return ''; @@ -534,6 +536,9 @@ define([ ' thumb-width="' + width + '"' + ' thumb-height="' + height + '"'; + swatchImageWidth = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.width : 30; + swatchImageHeight = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.height : 20; + if (!this.hasOwnProperty('products') || this.products.length <= 0) { attr += ' option-empty="true"'; } @@ -552,7 +557,7 @@ define([ // Image html += '<div class="' + optionClass + ' image" ' + attr + ' style="background: url(' + value + ') no-repeat center; background-size: initial;width:' + - sizeConfig.swatchImage.width + 'px; height:' + sizeConfig.swatchImage.height + 'px">' + '' + + swatchImageWidth + 'px; height:' + swatchImageHeight + 'px">' + '' + '</div>'; } else if (type === 3) { // Clear diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php index d1254790cb8eb..6ea14448c4aef 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php @@ -10,12 +10,37 @@ use Magento\Ui\Component\Control\ActionPool; use Magento\Ui\Component\Wrapper\UiComponent; use Magento\Ui\Controller\Adminhtml\AbstractAction; +use Magento\Backend\App\Action\Context; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponent\ContextFactory; +use Magento\Framework\App\ObjectManager; /** * Class Handle + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Handle extends AbstractAction implements HttpGetActionInterface { + /** + * @var ContextFactory + */ + private $contextFactory; + + /** + * @param Context $context + * @param UiComponentFactory $factory + * @param ContextFactory|null $contextFactory + */ + public function __construct( + Context $context, + UiComponentFactory $factory, + ContextFactory $contextFactory = null + ) { + parent::__construct($context, $factory); + $this->contextFactory = $contextFactory + ?: ObjectManager::getInstance()->get(ContextFactory::class); + } + /** * Render UI component by namespace in handle context * @@ -23,20 +48,50 @@ class Handle extends AbstractAction implements HttpGetActionInterface */ public function execute() { + $response = ''; $handle = $this->_request->getParam('handle'); $namespace = $this->_request->getParam('namespace'); $buttons = $this->_request->getParam('buttons', false); - $this->_view->loadLayout(['default', $handle], true, true, false); + $layout = $this->_view->getLayout(); + $context = $this->contextFactory->create( + [ + 'namespace' => $namespace, + 'pageLayout' => $layout + ] + ); - $uiComponent = $this->_view->getLayout()->getBlock($namespace); - $response = $uiComponent instanceof UiComponent ? $uiComponent->toHtml() : ''; + $component = $this->factory->create($namespace, null, ['context' => $context]); + if ($this->validateAclResource($component->getContext()->getDataProvider()->getConfigData())) { + $uiComponent = $layout->getBlock($namespace); + $response = $uiComponent instanceof UiComponent ? $uiComponent->toHtml() : ''; + } if ($buttons) { - $actionsToolbar = $this->_view->getLayout()->getBlock(ActionPool::ACTIONS_PAGE_TOOLBAR); + $actionsToolbar = $layout->getBlock(ActionPool::ACTIONS_PAGE_TOOLBAR); $response .= $actionsToolbar instanceof Template ? $actionsToolbar->toHtml() : ''; } $this->_response->appendBody($response); } + + /** + * Optionally validate ACL resource of components with a DataSource/DataProvider + * + * @param mixed $dataProviderConfigData + * @return bool + */ + private function validateAclResource($dataProviderConfigData) + { + if (isset($dataProviderConfigData['aclResource']) + && !$this->_authorization->isAllowed($dataProviderConfigData['aclResource']) + ) { + if (!$this->_request->isAjax()) { + $this->_redirect('admin/denied'); + } + return false; + } + + return true; + } } diff --git a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php index 31d5b421013f6..d31537458f213 100644 --- a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php +++ b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php @@ -6,7 +6,11 @@ namespace Magento\Ui\Test\Unit\Controller\Adminhtml\Index\Render; use Magento\Ui\Controller\Adminhtml\Index\Render\Handle; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class HandleTest extends \PHPUnit\Framework\TestCase { /** @@ -27,22 +31,42 @@ class HandleTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $componentFactoryMock; + protected $viewMock; + + /** + * @var Handle + */ + protected $controller; + + /** + * @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $viewMock; + private $uiComponentContextMock; /** - * @var Handle + * @var \Magento\Framework\View\Element\UiComponentInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $controller; + private $uiComponentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $uiFactoryMock; + + /** + * @var \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface| + * \PHPUnit_Framework_MockObject_MockObject + */ + private $dataProviderMock; public function setUp() { $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $this->componentFactoryMock = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); $this->contextMock->expects($this->atLeastOnce())->method('getRequest')->willReturn($this->requestMock); @@ -52,8 +76,37 @@ public function setUp() $this->viewMock = $this->createMock(\Magento\Framework\App\ViewInterface::class); $this->contextMock->expects($this->atLeastOnce())->method('getView')->willReturn($this->viewMock); - - $this->controller = new Handle($this->contextMock, $this->componentFactoryMock); + $this->authorizationMock = $this->getMockBuilder(\Magento\Framework\AuthorizationInterface::class) + ->getMockForAbstractClass(); + $this->authorizationMock->expects($this->any()) + ->method('isAllowed') + ->willReturn(true); + $this->uiComponentContextMock = $this->getMockForAbstractClass( + ContextInterface::class + ); + $this->uiComponentMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponentInterface::class + ); + $this->dataProviderMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class + ); + $this->uiComponentContextMock->expects($this->once()) + ->method('getDataProvider') + ->willReturn($this->dataProviderMock); + $this->uiFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiComponentMock->expects($this->any()) + ->method('getContext') + ->willReturn($this->uiComponentContextMock); + $this->uiFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->uiComponentMock); + $this->dataProviderMock->expects($this->once()) + ->method('getConfigData') + ->willReturn([]); + $contextMock = $this->createMock(\Magento\Framework\View\Element\UiComponent\ContextFactory::class); + $this->controller = new Handle($this->contextMock, $this->uiFactoryMock, $contextMock); } public function testExecuteNoButtons() @@ -83,7 +136,7 @@ public function testExecute() ->with(['default', $result], true, true, false); $layoutMock = $this->createMock(\Magento\Framework\View\LayoutInterface::class); - $this->viewMock->expects($this->exactly(2))->method('getLayout')->willReturn($layoutMock); + $this->viewMock->expects($this->once())->method('getLayout')->willReturn($layoutMock); $layoutMock->expects($this->exactly(2))->method('getBlock'); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js index 8f1a75d2be0d4..3b98d2c93c7a9 100755 --- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js @@ -57,6 +57,9 @@ define([ '${ $.provider }:${ $.customScope ? $.customScope + "." : ""}data.validate': 'validate', 'isUseDefault': 'toggleUseDefault' }, + ignoreTmpls: { + value: true + }, links: { value: '${ $.provider }:${ $.dataScope }' diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js index 18b3836113141..390aedf193b91 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js @@ -50,6 +50,9 @@ define([ } } }, + ignoreTmpls: { + data: true + }, listens: { elems: 'updateFields', data: 'updateState' diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml index 0d9df9176d2b5..3b9361f79871f 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml @@ -16,6 +16,9 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-69826"/> <group value="urlRewrite"/> + <skip> + <issueId value="MAGETWO-96713"/> + </skip> </annotations> <!-- Preconditions--> diff --git a/app/code/Magento/Variable/Model/Source/Variables.php b/app/code/Magento/Variable/Model/Source/Variables.php index 38c2f92d25545..e878a3172be4b 100644 --- a/app/code/Magento/Variable/Model/Source/Variables.php +++ b/app/code/Magento/Variable/Model/Source/Variables.php @@ -23,6 +23,16 @@ class Variables implements \Magento\Framework\Option\ArrayInterface */ private $configVariables = []; + /** + * @var array + */ + private $configPaths = []; + + /** + * @var \Magento\Config\Model\Config\Structure\SearchInterface + */ + private $configStructure; + /** * Constructor. * @@ -33,25 +43,8 @@ public function __construct( \Magento\Config\Model\Config\Structure\SearchInterface $configStructure, array $configPaths = [] ) { - foreach ($configPaths as $groupPath => $groupElements) { - $groupPathElements = explode('/', $groupPath); - $path = []; - $labels = []; - foreach ($groupPathElements as $groupPathElement) { - $path[] = $groupPathElement; - $labels[] = __( - $configStructure->getElementByConfigPath(implode('/', $path))->getLabel() - ); - } - $this->configVariables[$groupPath]['label'] = implode(' / ', $labels); - foreach (array_keys($groupElements) as $elementPath) { - $this->configVariables[$groupPath]['elements'][] = [ - 'value' => $elementPath, - 'label' => __($configStructure->getElementByConfigPath($elementPath)->getLabel()), - ]; - } - } - $this->configVariables; + $this->configStructure = $configStructure; + $this->configPaths = $configPaths; } /** @@ -64,7 +57,7 @@ public function toOptionArray($withGroup = false) { $optionArray = []; if ($withGroup) { - foreach ($this->configVariables as $configVariableGroup) { + foreach ($this->getConfigVariables() as $configVariableGroup) { $group = [ 'label' => $configVariableGroup['label'] ]; @@ -79,7 +72,7 @@ public function toOptionArray($withGroup = false) $optionArray[] = $group; } } else { - foreach ($this->configVariables as $configVariableGroup) { + foreach ($this->getConfigVariables() as $configVariableGroup) { foreach ($configVariableGroup['elements'] as $element) { $optionArray[] = [ 'value' => '{{config path="' . $element['value'] . '"}}', @@ -110,7 +103,7 @@ public function getData() private function getFlatConfigVars() { $result = []; - foreach ($this->configVariables as $configVariableGroup) { + foreach ($this->getConfigVariables() as $configVariableGroup) { foreach ($configVariableGroup['elements'] as $element) { $element['group_label'] = $configVariableGroup['label']; $result[] = $element; @@ -118,4 +111,35 @@ private function getFlatConfigVars() } return $result; } + + /** + * Merge config with user defined data + * + * @return array + */ + private function getConfigVariables() + { + if (empty($this->configVariables)) { + foreach ($this->configPaths as $groupPath => $groupElements) { + $groupPathElements = explode('/', $groupPath); + $path = []; + $labels = []; + foreach ($groupPathElements as $groupPathElement) { + $path[] = $groupPathElement; + $labels[] = __( + $this->configStructure->getElementByConfigPath(implode('/', $path))->getLabel() + ); + } + $this->configVariables[$groupPath]['label'] = implode(' / ', $labels); + foreach (array_keys($groupElements) as $elementPath) { + $this->configVariables[$groupPath]['elements'][] = [ + 'value' => $elementPath, + 'label' => __($this->configStructure->getElementByConfigPath($elementPath)->getLabel()), + ]; + } + } + } + + return $this->configVariables; + } } diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget.php b/app/code/Magento/Widget/Block/Adminhtml/Widget.php index e4854415d7534..33e6109b769db 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget.php @@ -15,7 +15,9 @@ class Widget extends \Magento\Backend\Block\Widget\Form\Container { /** - * @return void + * @inheritdoc + * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -40,13 +42,16 @@ protected function _construct() $this->buttonList->update('reset', 'label', __('Cancel')); $this->buttonList->update('reset', 'onclick', 'wWidget.closeModal()'); - $this->_formScripts[] = 'require(["mage/adminhtml/wysiwyg/widget"],' - . ' function(){wWidget = new WysiwygWidget.Widget(' - . '"widget_options_form", "select_widget_type", "widget_options", "' - . $this->getUrl( - 'adminhtml/*/loadOptions' - ) . '", "' . $this->getRequest()->getParam( - 'widget_target_id' - ) . '");});'; + $this->_formScripts[] = <<<EOJS +require(['mage/adminhtml/wysiwyg/widget'], function() { + wWidget = new WysiwygWidget.Widget( + 'widget_options_form', + 'select_widget_type', + 'widget_options', + '{$this->getUrl('adminhtml/*/loadOptions')}', + '{$this->escapeJs($this->getRequest()->getParam('widget_target_id'))}' + ); +}); +EOJS; } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less index 07050c1e5111d..08434727ccc9c 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less @@ -44,7 +44,6 @@ .page-actions { @_page-action__indent: 1.3rem; - float: right; .page-main-actions & { &._fixed { diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less index 8b727b0d9c28d..951ca89a07988 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less @@ -63,7 +63,6 @@ } &-actions { - display: none; .actions-secondary { > button.action { diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less index d0382d34d39fc..6bf766b7400a7 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less @@ -68,7 +68,6 @@ } &-actions { - display: none; .actions-secondary { > button.action { diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index 7c9f5b7a65ab4..ad677b45e52db 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -1506,6 +1506,10 @@ position: relative; z-index: 1; } + .limiter { + display: inline-block; + float: right; + } .toolbar-amount { .lib-css(line-height, @pager__line-height); diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 1017fb6716709..3e935e1d7ae9b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -159,62 +159,6 @@ private function loadWebsiteByCode($websiteCode) return $website; } - /** - * Test for check that 2 same product create and url_key save. - * - * @return void - */ - public function testSaveTwoSameProduct() - { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; - - $product1 = [ - 'product' => [ - 'attribute_set_id' => 4, - 'name' => "Test API 1", - 'price' => 254.13, - 'sku' => '1234' - ] - ]; - $product2 = [ - 'product' => [ - 'attribute_set_id' => 4, - 'name' => "Test API 1", - 'price' => 254.13, - 'sku' => '1235' - ] - ]; - - $product1 = $this->_webApiCall($serviceInfo, $product1); - $response = $this->_webApiCall($serviceInfo, $product2); - - $index = null; - foreach ($response['custom_attributes'] as $key => $customAttribute) { - if ($customAttribute['attribute_code'] == 'url_key') { - $index = $key; - break; - } - } - - $this->assertArrayHasKey(ProductInterface::SKU, $response); - - $expectedResult = $product1['custom_attributes'][$index]['value'] . '-1'; - $this->assertEquals($expectedResult, $response['custom_attributes'][$index]['value']); - - $this->deleteProduct('1234'); - $this->deleteProduct('1235'); - } - /** * Test removing association between product and website 1 * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php index eae0e600434a6..8262a7e41543e 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php @@ -115,8 +115,7 @@ public function testInvoke() ); $this->assertNotEmpty($result); $order = $this->objectManager->get(OrderRepositoryInterface::class)->get($order->getId()); - //Totally refunded orders still can be processed and shipped. - $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + $this->assertEquals(Order::STATE_CLOSED, $order->getState()); } private function getItemsForRest($order) diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php index a10a224604897..09c49bed1de83 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; class OrderGetTest extends WebapiAbstract @@ -18,19 +25,24 @@ class OrderGetTest extends WebapiAbstract const ORDER_INCREMENT_ID = '100000001'; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ - protected $objectManager; + private $objectManager; - protected function setUp() + /** + * @inheritdoc + */ + protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); } /** + * Checks order attributes. + * * @magentoApiDataFixture Magento/Sales/_files/order.php */ - public function testOrderGet() + public function testOrderGet(): void { $expectedOrderData = [ 'base_subtotal' => '100.0000', @@ -67,36 +79,21 @@ public function testOrderGet() 'region' => 'CA' ]; - /** @var \Magento\Sales\Model\Order $order */ - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId(self::ORDER_INCREMENT_ID); - - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $order->getId(), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_READ_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_READ_NAME . 'get', - ], - ]; - $result = $this->_webApiCall($serviceInfo, ['id' => $order->getId()]); + $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); foreach ($expectedOrderData as $field => $value) { - $this->assertArrayHasKey($field, $result); - $this->assertEquals($value, $result[$field]); + self::assertArrayHasKey($field, $result); + self::assertEquals($value, $result[$field]); } - $this->assertArrayHasKey('payment', $result); + self::assertArrayHasKey('payment', $result); foreach ($expectedPayments as $field => $value) { - $this->assertEquals($value, $result['payment'][$field]); + self::assertEquals($value, $result['payment'][$field]); } - $this->assertArrayHasKey('billing_address', $result); + self::assertArrayHasKey('billing_address', $result); foreach ($expectedBillingAddressNotEmpty as $field) { - $this->assertArrayHasKey($field, $result['billing_address']); + self::assertArrayHasKey($field, $result['billing_address']); } self::assertArrayHasKey('extension_attributes', $result); @@ -112,28 +109,86 @@ public function testOrderGet() //check that nullable fields were marked as optional and were not sent foreach ($result as $value) { - $this->assertNotNull($value); + self::assertNotNull($value); } } /** + * Checks order extension attributes. + * * @magentoApiDataFixture Magento/Sales/_files/order_with_tax.php */ - public function testOrderGetExtensionAttributes() + public function testOrderGetExtensionAttributes(): void { $expectedTax = [ 'code' => 'US-NY-*-Rate 1', 'type' => 'shipping' ]; - /** @var \Magento\Sales\Model\Order $order */ - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId(self::ORDER_INCREMENT_ID); + $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); + $appliedTaxes = $result['extension_attributes']['applied_taxes']; + self::assertEquals($expectedTax['code'], $appliedTaxes[0]['code']); + $appliedTaxes = $result['extension_attributes']['item_applied_taxes']; + self::assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); + self::assertNotEmpty($appliedTaxes[0]['applied_taxes']); + self::assertEquals(true, $result['extension_attributes']['converting_from_quote']); + } + + /** + * Checks if the order contains product option attributes. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_bundle.php + */ + public function testGetOrderWithProductOption(): void + { + $expected = [ + 'extension_attributes' => [ + 'bundle_options' => [ + [ + 'option_id' => 1, + 'option_selections' => [1], + 'option_qty' => 1 + ] + ] + ] + ]; + $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); + + $bundleProduct = $this->getBundleProduct($result['items']); + self::assertNotEmpty($bundleProduct, '"Bundle Product" should not be empty.'); + self::assertNotEmpty($bundleProduct['product_option'], '"Product Option" should not be empty.'); + self::assertEquals($expected, $bundleProduct['product_option']); + } + + /** + * Gets order by increment ID. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrder(string $incrementId): OrderInterface + { + /** @var Order $order */ + $order = $this->objectManager->create(Order::class); + $order->loadByIncrementId($incrementId); + + return $order; + } + + /** + * Makes service call. + * + * @param string $incrementId + * @return array + */ + private function makeServiceCall(string $incrementId): array + { + $order = $this->getOrder($incrementId); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $order->getId(), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_READ_NAME, @@ -141,13 +196,23 @@ public function testOrderGetExtensionAttributes() 'operation' => self::SERVICE_READ_NAME . 'get', ], ]; - $result = $this->_webApiCall($serviceInfo, ['id' => $order->getId()]); + return $this->_webApiCall($serviceInfo, ['id' => $order->getId()]); + } - $appliedTaxes = $result['extension_attributes']['applied_taxes']; - $this->assertEquals($expectedTax['code'], $appliedTaxes[0]['code']); - $appliedTaxes = $result['extension_attributes']['item_applied_taxes']; - $this->assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); - $this->assertNotEmpty($appliedTaxes[0]['applied_taxes']); - $this->assertEquals(true, $result['extension_attributes']['converting_from_quote']); + /** + * Gets a bundle product from the result. + * + * @param array $items + * @return array + */ + private function getBundleProduct(array $items): array + { + foreach ($items as $item) { + if ($item['product_type'] == 'bundle') { + return $item; + } + } + + return []; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php index aacda763ca2aa..12cbe1ca0e5e2 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php @@ -86,11 +86,10 @@ public function testShortRequest() 'Failed asserting that proper shipping amount of the Order was refunded' ); - //Totally refunded orders can be processed. - $this->assertEquals( + $this->assertNotEquals( $existingOrder->getStatus(), $updatedOrder->getStatus(), - 'Failed asserting that order status has not changed' + 'Failed asserting that order status was changed' ); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { $this->fail('Failed asserting that Creditmemo was created'); diff --git a/dev/tests/functional/tests/app/Magento/Backup/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Backup/Test/Repository/ConfigData.xml new file mode 100644 index 0000000000000..c8b19aa2bd32b --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backup/Test/Repository/ConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/Magento/Mtf/Repository/etc/repository.xsd"> + <repository class="Magento\Config\Test\Repository\ConfigData"> + <dataset name="enable_backups_functionality"> + <field name="system/backup/functionality_enabled" xsi:type="array"> + <item name="label" xsi:type="string">Yes</item> + <item name="value" xsi:type="number">1</item> + </field> + </dataset> + <dataset name="enable_backups_functionality_rollback"> + <field name="web/url/use_store" xsi:type="array"> + <item name="label" xsi:type="string">No</item> + <item name="value" xsi:type="number">0</item> + </field> + </dataset> + </repository> +</config> diff --git a/dev/tests/functional/tests/app/Magento/Backup/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Backup/Test/TestCase/NavigateMenuTest.xml deleted file mode 100644 index 0c024f0e3f5aa..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Backup/Test/TestCase/NavigateMenuTest.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> - <variation name="NavigateMenuTest7"> - <data name="menuItem" xsi:type="string">System > Backups</data> - <data name="pageTitle" xsi:type="string">Backups</data> - <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php index f79cf8d7eb7fa..0770de7f7d7a6 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php @@ -117,6 +117,7 @@ private function processLogin() if ($this->checkoutMethod === 'login') { if ($this->checkoutOnepage->getAuthenticationPopupBlock()->isVisible()) { $this->checkoutOnepage->getAuthenticationPopupBlock()->loginCustomer($this->customer); + sleep(5); $this->clickProceedToCheckoutStep->run(); } else { $this->checkoutOnepage->getLoginBlock()->loginCustomer($this->customer); diff --git a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml index 2b407e3d39200..86906d4c09406 100644 --- a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml +++ b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml @@ -17,7 +17,7 @@ <variation name="InstallTestVariation2" firstConstraint="Magento\Install\Test\Constraint\AssertSuccessInstall" summary="Install with custom encryption key and changed currency and locale"> <data name="user/dataset" xsi:type="string">default</data> <data name="install/keyOwn" xsi:type="string">I want to use my own encryption key</data> - <data name="install/keyValue" xsi:type="string">123123qa</data> + <data name="install/keyValue" xsi:type="string">I_want_to_use_my_own__encryption</data> <data name="install/storeLanguage" xsi:type="string">German (Germany)</data> <data name="install/storeCurrency" xsi:type="string">Euro (EUR)</data> <data name="currencySymbol" xsi:type="string">€</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionPartialFailMessage.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionPartialFailMessage.php index ecc9af83580f3..ed7596bf72f76 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionPartialFailMessage.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionPartialFailMessage.php @@ -16,15 +16,10 @@ */ class AssertOrderCancelMassActionPartialFailMessage extends AbstractConstraint { - /** - * Message displayed after cancel order from archive - */ - const SUCCESS_MESSAGE = 'We canceled 1 order(s).'; - /** * Text value to be checked */ - const FAIL_CANCEL_MESSAGE = '1 order(s) cannot be canceled.'; + const FAIL_CANCEL_MESSAGE = 'You cannot cancel the order(s).'; /** * Assert cancel fail message is displayed on order index page @@ -38,10 +33,6 @@ public function processAssert(OrderIndex $orderIndex) self::FAIL_CANCEL_MESSAGE, $orderIndex->getMessagesBlock()->getErrorMessage() ); - \PHPUnit\Framework\Assert::assertEquals( - self::SUCCESS_MESSAGE, - $orderIndex->getMessagesBlock()->getSuccessMessage() - ); } /** @@ -51,6 +42,6 @@ public function processAssert(OrderIndex $orderIndex) */ public function toString() { - return 'Cancel and success fail message is displayed on order index page.'; + return 'Cancel fail message is displayed on order index page.'; } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml index 31d5c30d25fc7..99e25b062846b 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml @@ -18,11 +18,11 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation2"> - <data name="description" xsi:type="string">try to cancel orders in status Complete, Canceled</data> + <data name="description" xsi:type="string">try to cancel orders in status Complete, Closed</data> <data name="steps" xsi:type="string">invoice, shipment|invoice, credit memo</data> <data name="action" xsi:type="string">Cancel</data> <data name="ordersCount" xsi:type="string">2</data> - <data name="resultStatuses" xsi:type="string">Complete,Canceled</data> + <data name="resultStatuses" xsi:type="string">Complete,Closed</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderCancelMassActionPartialFailMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> @@ -31,7 +31,7 @@ <data name="steps" xsi:type="string">invoice|invoice, credit memo</data> <data name="action" xsi:type="string">Cancel</data> <data name="ordersCount" xsi:type="string">2</data> - <data name="resultStatuses" xsi:type="string">Processing,Canceled</data> + <data name="resultStatuses" xsi:type="string">Processing,Closed</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderCancelMassActionPartialFailMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php index e867ebe399784..c1fe9ca45972d 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php @@ -12,6 +12,7 @@ use Magento\Backup\Test\Page\Adminhtml\BackupIndex; use Magento\Store\Test\Fixture\Store; use Magento\Mtf\TestCase\Injectable; +use Magento\Config\Test\TestStep\SetupConfigurationStep; /** * Test Creation for DeleteStoreEntity @@ -100,7 +101,15 @@ public function test(Store $store, $createBackup) { // Preconditions: $store->persist(); - $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); + $this->backupIndex->open() + ->getBackupGrid() + ->massaction([], 'Delete', true, 'Select All'); // Steps: $this->storeIndex->open(); diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php index cd37576443cdb..c332b83a22deb 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php @@ -12,6 +12,7 @@ use Magento\Backup\Test\Page\Adminhtml\BackupIndex; use Magento\Store\Test\Fixture\StoreGroup; use Magento\Mtf\TestCase\Injectable; +use Magento\Config\Test\TestStep\SetupConfigurationStep; /** * Delete StoreGroup (Store Management) @@ -101,7 +102,15 @@ public function test(StoreGroup $storeGroup, $createBackup) { //Preconditions $storeGroup->persist(); - $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); + $this->backupIndex->open() + ->getBackupGrid() + ->massaction([], 'Delete', true, 'Select All'); //Steps $this->storeIndex->open(); diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php index 2431cb3e065d2..22259a30b1538 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php @@ -12,6 +12,7 @@ use Magento\Backup\Test\Page\Adminhtml\BackupIndex; use Magento\Store\Test\Fixture\Website; use Magento\Mtf\TestCase\Injectable; +use Magento\Config\Test\TestStep\SetupConfigurationStep; /** * Delete Website (Store Management) @@ -102,7 +103,15 @@ public function test(Website $website, $createBackup) { //Preconditions $website->persist(); - $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); + $this->backupIndex->open() + ->getBackupGrid() + ->massaction([], 'Delete', true, 'Select All'); //Steps $this->storeIndex->open(); diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php index 155ed21064bcf..03486f337d74f 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php @@ -10,10 +10,12 @@ use Magento\Backend\Test\Page\Adminhtml\DeleteWebsite; use Magento\Backend\Test\Page\Adminhtml\StoreIndex; use Magento\Backup\Test\Page\Adminhtml\BackupIndex; +use Magento\Config\Test\TestStep\SetupConfigurationStep; use Magento\Store\Test\Fixture\Store; use Magento\Mtf\TestStep\TestStepInterface; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\Fixture\FixtureInterface; +use Magento\Mtf\TestStep\TestStepFactory; /** * Test Step for DeleteWebsitesEntity. @@ -60,6 +62,11 @@ class DeleteWebsitesEntityStep implements TestStepInterface */ private $createBackup; + /** + * @var TestStepFactory + */ + private $stepFactory; + /** * Prepare pages for test. * @@ -69,6 +76,7 @@ class DeleteWebsitesEntityStep implements TestStepInterface * @param DeleteWebsite $deleteWebsite * @param FixtureFactory $fixtureFactory * @param FixtureInterface $item + * @param TestStepFactory $testStepFactory * @param string $createBackup */ public function __construct( @@ -78,6 +86,7 @@ public function __construct( DeleteWebsite $deleteWebsite, FixtureFactory $fixtureFactory, FixtureInterface $item, + TestStepFactory $testStepFactory, $createBackup = 'No' ) { $this->storeIndex = $storeIndex; @@ -87,6 +96,7 @@ public function __construct( $this->item = $item; $this->createBackup = $createBackup; $this->fixtureFactory = $fixtureFactory; + $this->stepFactory = $testStepFactory; } /** @@ -96,6 +106,12 @@ public function __construct( */ public function run() { + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->stepFactory->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); $this->storeIndex->open(); $websiteNames = $this->item->getWebsiteIds(); diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php index 8147818135edc..a2767f76cfecb 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php @@ -85,6 +85,7 @@ public function test(Store $storeView, Category $childCategory, Category $parent $this->catalogCategoryEdit->getFormPageActions()->selectStoreView($storeView->getName()); $this->catalogCategoryEdit->getEditForm()->fill($categoryUpdates); $this->catalogCategoryEdit->getFormPageActions()->save(); + $this->catalogCategoryEdit->getFormPageActions()->selectStoreView('All Store Views'); $this->catalogCategoryIndex->getTreeCategories()->assignCategory( $parentCategory->getName(), $childCategory->getName() diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php index fa49a7b1f57e1..1fc07d32c77b9 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php @@ -64,6 +64,10 @@ protected function setUp() $filePath = $this->fullDirectoryPath . DIRECTORY_SEPARATOR . $this->fileName; $fixtureDir = realpath(__DIR__ . '/../../../../../Catalog/_files'); copy($fixtureDir . '/' . $this->fileName, $filePath); + $path = $this->fullDirectoryPath . '/.htaccess'; + if (!$this->mediaDirectory->isFile($path)) { + $this->mediaDirectory->writeFile($path, "Order deny,allow\nDeny from all"); + } $this->model = $this->objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFiles::class); } @@ -87,6 +91,26 @@ public function testExecute() ); } + /** + * Check that htaccess file couldn't be removed via + * \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFiles::execute method + * + * @return void + */ + public function testDeleteHtaccess() + { + $this->model->getRequest()->setMethod('POST') + ->setPostValue('files', [$this->imagesHelper->idEncode('.htaccess')]); + $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); + $this->model->execute(); + + $this->assertTrue( + $this->mediaDirectory->isExist( + $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . '/' . '.htaccess') + ) + ); + } + /** * Execute method with traversal file path to check that there is no ability to remove file which is not * under media directory. diff --git a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php index 9d149eceb4542..3ee68b1012c4e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php @@ -34,6 +34,7 @@ public static function setUpBeforeClass() /** * Test db backup includes triggers. * + * @magentoConfigFixture default/system/backup/functionality_enabled 1 * @magentoDataFixture Magento/Framework/Backup/_files/trigger.php * @magentoDbIsolation disabled */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/File/Validator/NotProtectedExtensionTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/File/Validator/NotProtectedExtensionTest.php new file mode 100644 index 0000000000000..622f17cbf9ce7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/File/Validator/NotProtectedExtensionTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Code\File\Validator; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class NotProtectedExtension + */ +class NotProtectedExtensionTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test that phpt, pht is invalid extension type + * @dataProvider isValidDataProvider + */ + public function testIsValid($extension) + { + $objectManager = Bootstrap::getObjectManager(); + /** @var \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $model */ + $model = $objectManager->create(\Magento\MediaStorage\Model\File\Validator\NotProtectedExtension::class); + $this->assertFalse($model->isValid($extension)); + } + + public function isValidDataProvider() + { + return [ + ['phpt'], + ['pht'] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php index 552a23a0c1fd5..9afce0ed10bcd 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php @@ -17,12 +17,15 @@ class ValidateTest extends \Magento\TestFramework\TestCase\AbstractBackendContro /** * @dataProvider validationDataProvider * @param string $fileName + * @param string $mimeType * @param string $message * @param string $delimiter + * @throws \Magento\Framework\Exception\FileSystemException * @backupGlobals enabled * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.Superglobals) */ - public function testValidationReturn($fileName, $message, $delimiter) + public function testValidationReturn(string $fileName, string $mimeType, string $message, string $delimiter) { $validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR; @@ -50,7 +53,7 @@ public function testValidationReturn($fileName, $message, $delimiter) $_FILES = [ 'import_file' => [ 'name' => $fileName, - 'type' => 'text/csv', + 'type' => $mimeType, 'tmp_name' => $target, 'error' => 0, 'size' => filesize($target) @@ -84,24 +87,34 @@ public function validationDataProvider() return [ [ 'file_name' => 'catalog_product.csv', + 'mime-type' => 'text/csv', 'message' => 'File is valid', 'delimiter' => ',', ], [ 'file_name' => 'test.txt', + 'mime-type' => 'text/csv', 'message' => '\'txt\' file extension is not supported', 'delimiter' => ',', ], [ 'file_name' => 'incorrect_catalog_product_comma.csv', + 'mime-type' => 'text/csv', 'message' => 'Download full report', 'delimiter' => ',', ], [ 'file_name' => 'incorrect_catalog_product_semicolon.csv', + 'mime-type' => 'text/csv', 'message' => 'Download full report', 'delimiter' => ';', ], + [ + 'file_name' => 'catalog_product.zip', + 'mime-type' => 'application/zip', + 'message' => 'File is valid', + 'delimiter' => ',', + ], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/catalog_product.zip b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/catalog_product.zip new file mode 100644 index 0000000000000..812beae22b786 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/catalog_product.zip differ diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php index f2cd745e96d83..1a22ea947f85a 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php @@ -64,8 +64,7 @@ public function testProcessIpnRequestFullRefund() $creditmemoItems = $order->getCreditmemosCollection()->getItems(); $creditmemo = current($creditmemoItems); - //Totally refunded orders still can be shipped - $this->assertEquals(Order::STATE_PROCESSING, $order->getState()) ; + $this->assertEquals(Order::STATE_CLOSED, $order->getState()) ; $this->assertEquals(1, count($creditmemoItems)); $this->assertEquals(Creditmemo::STATE_REFUNDED, $creditmemo->getState()); $this->assertEquals(10, $order->getSubtotalRefunded()); @@ -147,8 +146,7 @@ public function testProcessIpnRequestRestRefund() $creditmemoItems = $order->getCreditmemosCollection()->getItems(); - //Totally refunded orders still can be shipped - $this->assertEquals(Order::STATE_PROCESSING, $order->getState()) ; + $this->assertEquals(Order::STATE_CLOSED, $order->getState()) ; $this->assertEquals(1, count($creditmemoItems)); $this->assertEquals(10, $order->getSubtotalRefunded()); $this->assertEquals(10, $order->getBaseSubtotalRefunded()); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php index 0679fc6ffe6cb..1d04a79ae3f84 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php @@ -90,10 +90,11 @@ public function testAddTrack() $items[$item->getId()] = $item->getQtyOrdered(); } /** @var \Magento\Sales\Model\Order\Shipment $shipment */ - $shipment = $this->objectManager->get(ShipmentFactory::class)->create($order, $items); + $shipment = $this->objectManager->get(ShipmentFactory::class) + ->create($order, $items); $shipment->addTrack($track); - $shipment->save(); - $saved = $this->shipmentRepository->save($shipment); + $this->shipmentRepository->save($shipment); + $saved = $this->shipmentRepository->get((int)$shipment->getEntityId()); self::assertNotEmpty($saved->getTracks()); } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle.php index 6b6d6d6e6ec04..4473fb8dfe72c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle.php @@ -6,7 +6,6 @@ declare(strict_types=1); use Magento\Sales\Api\Data\OrderItemInterface; -use Magento\Sales\Api\OrderItemRepositoryInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Item; @@ -29,6 +28,10 @@ OrderItemInterface::PRODUCT_TYPE => 'bundle', 'product_options' => [ 'product_calculations' => 0, + 'info_buyRequest' => [ + 'bundle_option' => [1 => 1], + 'bundle_option_qty' => 1, + ] ], 'children' => [ [ diff --git a/dev/tests/integration/testsuite/Magento/Ui/Controller/Adminhtml/Index/Renderer/HandleTest.php b/dev/tests/integration/testsuite/Magento/Ui/Controller/Adminhtml/Index/Renderer/HandleTest.php new file mode 100644 index 0000000000000..dbe721a76e2c6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Controller/Adminhtml/Index/Renderer/HandleTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ui\Controller\Adminhtml\Index\Renderer; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\AuthorizationInterface; + +/** + * @magentoAppArea adminhtml + */ +class HandleTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecuteWhenUserDoesNotHavePermission() + { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + AuthorizationInterface::class => \Magento\Ui\Model\AuthorizationMock::class + ] + ]); + $this->getRequest()->setParam('handle', 'customer_index_index'); + $this->getRequest()->setParam('namespace', 'customer_listing'); + $this->getRequest()->setParam('sorting%5Bfield%5D', 'entity_id'); + $this->getRequest()->setParam('paging%5BpageSize%5D', 20); + $this->getRequest()->setParam('isAjax', 1); + $this->dispatch('backend/mui/index/render_handle'); + $output = $this->getResponse()->getBody(); + $this->assertEmpty($output, 'The acl restriction wasn\'t applied properly'); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecuteWhenUserHasPermission() + { + $this->getRequest()->setParam('handle', 'customer_index_index'); + $this->getRequest()->setParam('namespace', 'customer_listing'); + $this->getRequest()->setParam('sorting%5Bfield%5D', 'entity_id'); + $this->getRequest()->setParam('paging%5BpageSize%5D', 20); + $this->getRequest()->setParam('isAjax', 1); + $this->dispatch('backend/mui/index/render_handle'); + $output = $this->getResponse()->getBody(); + $this->assertNotEmpty($output, 'The acl restriction wasn\'t applied properly'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ui/Model/AuthorizationMock.php b/dev/tests/integration/testsuite/Magento/Ui/Model/AuthorizationMock.php new file mode 100644 index 0000000000000..76f472f30eb1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Model/AuthorizationMock.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ui\Model; + +/** + * Class AuthorizationMock + */ +class AuthorizationMock extends \Magento\Framework\Authorization +{ + /** + * Check current user permission on resource and privilege + * + * @param string $resource + * @param string $privilege + * @return boolean + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function isAllowed($resource, $privilege = null) + { + return $resource !== 'Magento_Customer::manage'; + } +} diff --git a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js index 1e1203d22a1e3..ca9ec877013f8 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js @@ -1142,4 +1142,24 @@ define([ )).toEqual(true); }); }); + + describe('Testing validate-forbidden-extensions', function () { + it('validate-forbidden-extensions', function () { + var el1 = $('<input type="text" value="" ' + + 'class="validate-extensions" data-validation-params="php,phtml">').get(0); + + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'php', el1, null)).toEqual(false); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'php,phtml', el1, null)).toEqual(false); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'html', el1, null)).toEqual(true); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'html,png', el1, null)).toEqual(true); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'php,html', el1, null)).toEqual(false); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'html,php', el1, null)).toEqual(false); + }); + }); }); diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/RequestAwareBlockMethod.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/RequestAwareBlockMethod.php new file mode 100644 index 0000000000000..9ce891da718b4 --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/RequestAwareBlockMethod.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CodeMessDetector\Rule\Design; + +use PHPMD\AbstractNode; +use PHPMD\AbstractRule; +use PHPMD\Node\ClassNode; +use PHPMD\Node\MethodNode; +use PDepend\Source\AST\ASTMethod; +use PHPMD\Rule\MethodAware; + +/** + * Detect direct request usages. + */ +class RequestAwareBlockMethod extends AbstractRule implements MethodAware +{ + /** + * @inheritDoc + * + * @param ASTMethod|MethodNode $method + */ + public function apply(AbstractNode $method) + { + $definedIn = $method->getParentType(); + try { + $isBlock = ($definedIn instanceof ClassNode) + && is_subclass_of( + $definedIn->getFullQualifiedName(), + \Magento\Framework\View\Element\AbstractBlock::class + ); + } catch (\Throwable $exception) { + //Failed to load classes. + return; + } + + if ($isBlock) { + $nodes = $method->findChildrenOfType('PropertyPostfix') + $method->findChildrenOfType('MethodPostfix'); + foreach ($nodes as $node) { + $name = mb_strtolower($node->getFirstChildOfType('Identifier')->getImage()); + if ($name === '_request' || $name === 'getrequest') { + $this->addViolation($method, [$method->getFullQualifiedName()]); + break; + } + } + } + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml index c9bfe4fe6e308..100e08276e6cf 100644 --- a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml +++ b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml @@ -54,6 +54,37 @@ class PostOrder implements ActionInterface ... return $response; } +} + ]]> + </example> + </rule> + <rule name="RequestAwareBlockMethod" + class="Magento\CodeMessDetector\Rule\Design\RequestAwareBlockMethod" + message="{0} uses request object directly. Add user input validation and suppress this warning."> + <description> + <![CDATA[ +Blocks must not depend on being used with certain controllers. +If you use request object in a block directly you must validate all user input inside the block. + ]]> + </description> + <priority>2</priority> + <properties /> + <example> + <![CDATA[ +class MyOrder extends AbstractBlock +{ + + ....... + + public function getOrder() + { + $orderId = $this->getRequest()->getParam('order_id'); + //Validate customer having such order. + if (!$this->hasOrder($this->getCustomerId(), $orderId)) { + ...deny access... + } + ..... + } } ]]> </example> diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml index fddb1e6fdfc14..2d9eb7478ce91 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml @@ -48,5 +48,6 @@ <!-- Magento Specific Rules --> <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/FinalImplementation" /> <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/AllPurposeAction" /> + <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/RequestAwareBlockMethod" /> </ruleset> diff --git a/lib/internal/Magento/Framework/Archive/Zip.php b/lib/internal/Magento/Framework/Archive/Zip.php index f33ad8700f056..c41f8b28ce348 100644 --- a/lib/internal/Magento/Framework/Archive/Zip.php +++ b/lib/internal/Magento/Framework/Archive/Zip.php @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Class to work with zip archives - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Framework\Archive; +/** + * Zip compressed file archive. + */ class Zip extends AbstractArchive implements ArchiveInterface { /** @@ -54,11 +52,34 @@ public function pack($source, $destination) public function unpack($source, $destination) { $zip = new \ZipArchive(); - $zip->open($source); - $filename = $zip->getNameIndex(0); - $zip->extractTo(dirname($destination), $filename); - rename(dirname($destination).'/'.$filename, $destination); - $zip->close(); + if ($zip->open($source) === true) { + $filename = $this->filterRelativePaths($zip->getNameIndex(0) ?: ''); + if ($filename) { + $zip->extractTo(dirname($destination), $filename); + rename(dirname($destination).'/'.$filename, $destination); + } else { + $destination = ''; + } + $zip->close(); + } else { + $destination = ''; + } + return $destination; } + + /** + * Filter file names with relative paths. + * + * @param string $path + * @return string + */ + private function filterRelativePaths(string $path): string + { + if ($path && preg_match('#^\s*(../)|(/../)#i', $path)) { + $path = ''; + } + + return $path; + } } diff --git a/lib/internal/Magento/Framework/Backup/BackupInterface.php b/lib/internal/Magento/Framework/Backup/BackupInterface.php index 3d054bdbd1a9c..16aada9689c11 100644 --- a/lib/internal/Magento/Framework/Backup/BackupInterface.php +++ b/lib/internal/Magento/Framework/Backup/BackupInterface.php @@ -13,6 +13,8 @@ /** * @api + * + * @deprecated Backups should be done using other means. */ interface BackupInterface { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php b/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php index 13e3c562bb527..a019ccac06b66 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php @@ -7,6 +7,8 @@ /** * @api + * + * @deprecated Backups should be done using other means. */ interface BackupDbInterface { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php b/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php index f7459f629cb4a..ae5879290eb20 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php @@ -7,6 +7,8 @@ /** * @api + * + * @deprecated Backups should be done using other means. */ interface BackupInterface { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Text.php b/lib/internal/Magento/Framework/Data/Form/Element/Text.php index eb157c7279a71..2f001eb10307b 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Text.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Text.php @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Form text element - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Framework\Data\Form\Element; use Magento\Framework\Escaper; +/** + * Form text element + */ class Text extends AbstractElement { /** @@ -65,6 +63,7 @@ public function getHtmlAttributes() 'placeholder', 'data-form-part', 'data-role', + 'data-validation-params', 'data-action' ]; } diff --git a/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php b/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php index 3660b28657b58..c2e82a6a7f6f8 100644 --- a/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php +++ b/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php @@ -144,9 +144,11 @@ public function getHandle() */ public function encrypt(string $data): string { - throw new \Exception( - (string)new \Magento\Framework\Phrase('Mcrypt cannot be used for encryption. Use Sodium instead') - ); + if (strlen($data) == 0) { + return $data; + } + // @codingStandardsIgnoreLine + return @mcrypt_generic($this->getHandle(), $data); } /** diff --git a/lib/internal/Magento/Framework/Encryption/Encryptor.php b/lib/internal/Magento/Framework/Encryption/Encryptor.php index f7d52d5474ad7..192782e0a1f6f 100644 --- a/lib/internal/Magento/Framework/Encryption/Encryptor.php +++ b/lib/internal/Magento/Framework/Encryption/Encryptor.php @@ -228,6 +228,8 @@ public function validateHashVersion($hash, $validateCount = false) } /** + * Split password hash into parts: hash, salt, version + * * @param string $hash * @return array */ @@ -243,6 +245,8 @@ private function explodePasswordHash($hash) } /** + * Get password hash + * * @return string */ private function getPasswordHash() @@ -251,6 +255,8 @@ private function getPasswordHash() } /** + * Get password salt + * * @return string */ private function getPasswordSalt() @@ -259,6 +265,8 @@ private function getPasswordSalt() } /** + * Get password version + * * @return array */ private function getPasswordVersion() @@ -281,6 +289,22 @@ public function encrypt($data) ':' . base64_encode($crypt->encrypt($data)); } + /** + * Encrypt data using the fastest available algorithm + * + * @param string $data + * @return string + */ + public function encryptWithFastestAvailableAlgorithm($data) + { + $crypt = $this->getCrypt(); + if (null === $crypt) { + return $data; + } + return $this->keyVersion . + ':' . $this->getCipherVersion() . + ':' . base64_encode($crypt->encrypt($data)); + } /** * Look for key and crypt versions in encrypted data before decrypting * @@ -391,7 +415,7 @@ private function getCrypt( string $initVector = null ): ?EncryptionAdapterInterface { if (null === $key && null === $cipherVersion) { - $cipherVersion = self::CIPHER_RIJNDAEL_256; + $cipherVersion = $this->getCipherVersion(); } if (null === $key) { @@ -424,4 +448,18 @@ private function getCrypt( return new Mcrypt($key, $cipher, $mode, $initVector); } + + /** + * Get cipher version + * + * @return int + */ + private function getCipherVersion() + { + if (extension_loaded('sodium')) { + return $this->cipher; + } else { + return self::CIPHER_RIJNDAEL_256; + } + } } diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php index 2622441aa0896..452357003630c 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php @@ -160,15 +160,6 @@ public function testDecrypt( $this->assertEquals($expectedData, $actualData); } - /** - * @expectedException \Exception - */ - public function testEncrypt() - { - $crypt = new \Magento\Framework\Encryption\Adapter\Mcrypt($this->key); - $crypt->encrypt('Hello World!!'); - } - /** * @dataProvider getCipherModeCombinations */ diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php index f3853a123c156..98bb1c5676d6c 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php @@ -238,7 +238,7 @@ public function testValidateKeyInvalid() /** * @return array */ - public function testUseSpecifiedHashingAlgoDataProvider() + public function useSpecifiedHashingAlgoDataProvider() { return [ ['password', 'salt', Encryptor::HASH_VERSION_MD5, @@ -253,7 +253,7 @@ public function testUseSpecifiedHashingAlgoDataProvider() } /** - * @dataProvider testUseSpecifiedHashingAlgoDataProvider + * @dataProvider useSpecifiedHashingAlgoDataProvider * * @param $password * @param $salt diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index fec64378189eb..80979c3db0910 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework; /** @@ -32,13 +33,23 @@ class Escaper */ private $allowedAttributes = ['id', 'class', 'href', 'target', 'title', 'style']; + /** + * @var string + */ + private static $xssFiltrationPattern = + '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|' + . '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|' + . '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i'; + /** * @var string[] */ private $escapeAsUrlAttributes = ['href']; /** - * Escape string for HTML context. allowedTags will not be escaped, except the following: script, img, embed, + * Escape string for HTML context. + * + * AllowedTags will not be escaped, except the following: script, img, embed, * iframe, video, source, object, audio * * @param string|array $data @@ -47,6 +58,10 @@ class Escaper */ public function escapeHtml($data, $allowedTags = null) { + if (!is_array($data)) { + $data = (string)$data; + } + if (is_array($data)) { $result = []; foreach ($data as $item) { @@ -54,16 +69,7 @@ public function escapeHtml($data, $allowedTags = null) } } elseif (strlen($data)) { if (is_array($allowedTags) && !empty($allowedTags)) { - $notAllowedTags = array_intersect( - array_map('strtolower', $allowedTags), - $this->notAllowedTags - ); - if (!empty($notAllowedTags)) { - $this->getLogger()->critical( - 'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags) - ); - $allowedTags = array_diff($allowedTags, $this->notAllowedTags); - } + $allowedTags = $this->filterProhibitedTags($allowedTags); $wrapperElementId = uniqid(); $domDocument = new \DOMDocument('1.0', 'UTF-8'); set_error_handler( @@ -197,7 +203,7 @@ public function escapeHtmlAttr($string, $escapeSingleQuote = true) if ($escapeSingleQuote) { return $this->getEscaper()->escapeHtmlAttr((string) $string); } - return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false); + return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8', false); } /** @@ -278,14 +284,13 @@ public function escapeJsQuote($data, $quote = '\'') $result[] = $this->escapeJsQuote($item, $quote); } } else { - $result = str_replace($quote, '\\' . $quote, $data); + $result = str_replace($quote, '\\' . $quote, (string)$data); } return $result; } /** * Escape xss in urls - * Remove `javascript:`, `vbscript:`, `data:` words from url * * @param string $data * @return string @@ -293,15 +298,33 @@ public function escapeJsQuote($data, $quote = '\'') */ public function escapeXssInUrl($data) { - $pattern = '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|' - . '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|' - . '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i'; - $result = preg_replace($pattern, ':', $data); - return htmlspecialchars($result, ENT_COMPAT | ENT_HTML5 | ENT_HTML401, 'UTF-8', false); + return htmlspecialchars( + $this->escapeScriptIdentifiers((string)$data), + ENT_COMPAT | ENT_HTML5 | ENT_HTML401, + 'UTF-8', + false + ); + } + + /** + * Remove `javascript:`, `vbscript:`, `data:` words from the string. + * + * @param string $data + * @return string + */ + private function escapeScriptIdentifiers(string $data): string + { + $filteredData = preg_replace(self::$xssFiltrationPattern, ':', $data) ?: ''; + if (preg_match(self::$xssFiltrationPattern, $filteredData)) { + $filteredData = $this->escapeScriptIdentifiers($filteredData); + } + + return $filteredData; } /** * Escape quotes inside html attributes + * * Use $addSlashes = false for escaping js that inside html attribute (onClick, onSubmit etc) * * @param string $data @@ -346,4 +369,27 @@ private function getLogger() } return $this->logger; } + + /** + * Filter prohibited tags. + * + * @param string[] $allowedTags + * @return string[] + */ + private function filterProhibitedTags(array $allowedTags): array + { + $notAllowedTags = array_intersect( + array_map('strtolower', $allowedTags), + $this->notAllowedTags + ); + + if (!empty($notAllowedTags)) { + $this->getLogger()->critical( + 'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags) + ); + $allowedTags = array_diff($allowedTags, $this->notAllowedTags); + } + + return $allowedTags; + } } diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php b/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php index 33007b7295bca..e0dc7494cca19 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php @@ -4,10 +4,6 @@ * See COPYING.txt for license details. */ return [ - 'without_event_handle' => [ - '<?xml version="1.0"?><config></config>', - ["Element 'config': Missing child element(s). Expected is ( event ).\nLine: 1\n"], - ], 'event_without_required_name_attribute' => [ '<?xml version="1.0"?><config><event name="some_name"></event></config>', ["Element 'event': Missing child element(s). Expected is ( observer ).\nLine: 1\n"], diff --git a/lib/internal/Magento/Framework/Event/etc/events.xsd b/lib/internal/Magento/Framework/Event/etc/events.xsd index d656b7fdb6ed6..cac62af356760 100644 --- a/lib/internal/Magento/Framework/Event/etc/events.xsd +++ b/lib/internal/Magento/Framework/Event/etc/events.xsd @@ -9,7 +9,7 @@ <xs:element name="config"> <xs:complexType> <xs:sequence> - <xs:element name="event" type="eventDeclaration" minOccurs="1" maxOccurs="unbounded"> + <xs:element name="event" type="eventDeclaration" minOccurs="0" maxOccurs="unbounded"> <xs:unique name="uniqueObserverName"> <xs:annotation> <xs:documentation> diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 0785177042451..8608f20285db3 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -207,20 +207,22 @@ public function save($destinationFolder, $newFileName = null) $this->_result = false; $destinationFile = $destinationFolder; $fileName = isset($newFileName) ? $newFileName : $this->_file['name']; - $fileName = self::getCorrectFileName($fileName); + $fileName = static::getCorrectFileName($fileName); if ($this->_enableFilesDispersion) { $fileName = $this->correctFileNameCase($fileName); $this->setAllowCreateFolders(true); - $this->_dispretionPath = self::getDispersionPath($fileName); + $this->_dispretionPath = static::getDispersionPath($fileName); $destinationFile .= $this->_dispretionPath; $this->_createDestinationFolder($destinationFile); } if ($this->_allowRenameFiles) { - $fileName = self::getNewFileName(self::_addDirSeparator($destinationFile) . $fileName); + $fileName = static::getNewFileName( + static::_addDirSeparator($destinationFile) . $fileName + ); } - $destinationFile = self::_addDirSeparator($destinationFile) . $fileName; + $destinationFile = static::_addDirSeparator($destinationFile) . $fileName; try { $this->_result = $this->_moveFile($this->_file['tmp_name'], $destinationFile); diff --git a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php index e21841b48bc13..a4f728454a524 100644 --- a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php +++ b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php @@ -30,7 +30,7 @@ class PluginList extends Scoped implements InterceptionPluginList * * @var array */ - protected $_inherited; + protected $_inherited = []; /** * Inherited plugin data, preprocessed for read diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index b2f857bf29f45..335006555d2f1 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -952,7 +952,7 @@ public function stripTags($data, $allowableTags = null, $allowHtmlEntities = fal */ public function escapeUrl($string) { - return $this->_escaper->escapeUrl($string); + return $this->_escaper->escapeUrl((string)$string); } /** @@ -1156,6 +1156,7 @@ public function getVar($name, $module = null) /** * Determine if the block scope is private or public. + * * Returns true if scope is private, false otherwise * * @return bool diff --git a/lib/internal/Magento/Framework/View/Element/Template/File/Validator.php b/lib/internal/Magento/Framework/View/Element/Template/File/Validator.php index 4b4e7e1bad467..285c05c9db69c 100644 --- a/lib/internal/Magento/Framework/View/Element/Template/File/Validator.php +++ b/lib/internal/Magento/Framework/View/Element/Template/File/Validator.php @@ -7,10 +7,10 @@ use \Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Component\ComponentRegistrar; +use \Magento\Framework\Filesystem\Driver\File as FileDriver; /** * Class Validator - * @package Magento\Framework\View\Element\Template\File */ class Validator { @@ -68,6 +68,11 @@ class Validator */ protected $_compiledDir; + /** + * @var FileDriver + */ + private $fileDriver; + /** * Class constructor * @@ -75,12 +80,14 @@ class Validator * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface * @param ComponentRegistrar $componentRegistrar * @param string|null $scope + * @param FileDriver|null $fileDriver */ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface, ComponentRegistrar $componentRegistrar, - $scope = null + $scope = null, + ?FileDriver $fileDriver = null ) { $this->_filesystem = $filesystem; $this->_isAllowSymlinks = $scopeConfigInterface->getValue(self::XML_PATH_TEMPLATE_ALLOW_SYMLINK, $scope); @@ -88,6 +95,7 @@ public function __construct( $this->moduleDirs = $componentRegistrar->getPaths(ComponentRegistrar::MODULE); $this->_compiledDir = $this->_filesystem->getDirectoryRead(DirectoryList::TMP_MATERIALIZATION_DIR) ->getAbsolutePath(); + $this->fileDriver = $fileDriver ?: \Magento\Framework\App\ObjectManager::getInstance()->get(FileDriver::class); } /** @@ -127,8 +135,9 @@ protected function isPathInDirectories($path, $directories) if (!is_array($directories)) { $directories = (array)$directories; } + $realPath = $this->fileDriver->getRealPath($path); foreach ($directories as $directory) { - if (0 === strpos($path, $directory)) { + if (0 === strpos($realPath, $directory)) { return true; } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Template/File/ValidatorTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Template/File/ValidatorTest.php index 28d58f4685e80..7a3578993f375 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Template/File/ValidatorTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Template/File/ValidatorTest.php @@ -20,21 +20,21 @@ class ValidatorTest extends \PHPUnit\Framework\TestCase * * @var \Magento\Framework\View\Element\Template\File\Validator */ - private $_validator; + private $validator; /** * Mock for view file system * * @var \Magento\Framework\FileSystem|\PHPUnit_Framework_MockObject_MockObject */ - private $_fileSystemMock; + private $fileSystemMock; /** * Mock for scope config * * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $_scopeConfigMock; + private $scopeConfigMock; /** * Mock for root directory reader @@ -62,12 +62,12 @@ class ValidatorTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->_fileSystemMock = $this->createMock(\Magento\Framework\Filesystem::class); - $this->_scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $this->fileSystemMock = $this->createMock(\Magento\Framework\Filesystem::class); + $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->rootDirectoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadInterface::class); $this->compiledDirectoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $this->_fileSystemMock->expects($this->any()) + $this->fileSystemMock->expects($this->any()) ->method('getDirectoryRead') ->will($this->returnValueMap( [ @@ -91,10 +91,18 @@ protected function setUp() ] ) ); - $this->_validator = new \Magento\Framework\View\Element\Template\File\Validator( - $this->_fileSystemMock, - $this->_scopeConfigMock, - $this->componentRegistrar + + $fileDriverMock = $this->createMock(\Magento\Framework\Filesystem\Driver\File::class); + $fileDriverMock->expects($this->any()) + ->method('getRealPath') + ->willReturnArgument(0); + + $this->validator = new \Magento\Framework\View\Element\Template\File\Validator( + $this->fileSystemMock, + $this->scopeConfigMock, + $this->componentRegistrar, + null, + $fileDriverMock ); } @@ -103,23 +111,22 @@ protected function setUp() * * @param string $file * @param bool $expectedResult - * - * @dataProvider testIsValidDataProvider - * * @return void + * + * @dataProvider isValidDataProvider */ public function testIsValid($file, $expectedResult) { $this->rootDirectoryMock->expects($this->any())->method('isFile')->will($this->returnValue(true)); - $this->assertEquals($expectedResult, $this->_validator->isValid($file)); + $this->assertEquals($expectedResult, $this->validator->isValid($file)); } /** * Data provider for testIsValid * - * @return [] + * @return array */ - public function testIsValidDataProvider() + public function isValidDataProvider() { return [ 'empty' => ['', false], diff --git a/lib/web/mage/gallery/gallery.less b/lib/web/mage/gallery/gallery.less index 3c35a772253bc..ebd6bdd003b81 100644 --- a/lib/web/mage/gallery/gallery.less +++ b/lib/web/mage/gallery/gallery.less @@ -764,6 +764,7 @@ max-width: inherit; position: absolute; top: 0; + object-fit: scale-down; } } diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 0fd28143ea9a0..c33e787b29254 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -887,6 +887,27 @@ }, $.mage.__('Please enter a valid number in this field.') ], + 'validate-forbidden-extensions': [ + function (v, elem) { + var forbiddenExtensions = $(elem).attr('data-validation-params'), + forbiddenExtensionsArray = forbiddenExtensions.split(','), + extensionsArray = v.split(','), + result = true; + + this.validateExtensionsMessage = $.mage.__('Forbidden extensions has been used. Avoid usage of ') + + forbiddenExtensions; + + $.each(extensionsArray, function (key, extension) { + if (forbiddenExtensionsArray.indexOf(extension) !== -1) { + result = false; + } + }); + + return result; + }, function () { + return this.validateExtensionsMessage; + } + ], 'validate-digits-range': [ function (v, elm, param) { var numValue, dataAttrRange, classNameRange, result, range, m, classes, ii; diff --git a/lib/web/magnifier/magnifier.js b/lib/web/magnifier/magnifier.js index 55646aa9b4af2..06e41377ae33f 100644 --- a/lib/web/magnifier/magnifier.js +++ b/lib/web/magnifier/magnifier.js @@ -542,7 +542,11 @@ showWrapper = true; bindEvents(eventType, thumb); data[idx].status = 2; - data[idx].zoom = largeObj.height / largeWrapper.height(); + if (largeObj.width > largeObj.height) { + data[idx].zoom = largeObj.width / largeWrapper.width(); + } else { + data[idx].zoom = largeObj.height / largeWrapper.height(); + } setThumbData(thumb, data[idx]); updateLensOnLoad(idx, thumb, largeObj, largeWrapper); } diff --git a/nginx.conf.sample b/nginx.conf.sample index 6f87a9a076666..90604808f6ec0 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -161,7 +161,7 @@ location /media/import/ { } # PHP entry point for main application -location ~ (index|get|static|report|404|503|health_check)\.php$ { +location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { try_files $uri =404; fastcgi_pass fastcgi_backend; fastcgi_buffers 1024 4k; diff --git a/setup/src/Magento/Setup/Model/CryptKeyGenerator.php b/setup/src/Magento/Setup/Model/CryptKeyGenerator.php index ec9d3f028126a..df72dc4c6b8bb 100644 --- a/setup/src/Magento/Setup/Model/CryptKeyGenerator.php +++ b/setup/src/Magento/Setup/Model/CryptKeyGenerator.php @@ -31,10 +31,8 @@ public function __construct(Random $random) /** * Generates & returns a string to be used as crypt key. - * The key length is not a parameter, but an implementation detail. * * @return string - * * @throws \Magento\Framework\Exception\LocalizedException */ public function generate()