diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
index d37b755e8c57a..6257faf980618 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
@@ -306,6 +306,9 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid',
ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually',
ValidatorInterface::ERROR_NEW_TO_DATE => 'Make sure new_to_date is later than or the same as new_from_date',
+ // Can't add new translated strings in patch release
+ 'invalidLayoutUpdate' => 'Invalid format.',
+ 'insufficientPermissions' => 'Invalid format.',
];
/**
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php
new file mode 100644
index 0000000000000..cd73fac89855a
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php
@@ -0,0 +1,83 @@
+layoutValidatorFactory = $layoutValidatorFactory;
+ $this->validationState = $validationState;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isValid($value): bool
+ {
+ if (!empty($value['custom_layout_update']) && !$this->validateXml($value['custom_layout_update'])) {
+ $this->_addMessages(
+ [
+ $this->context->retrieveMessageTemplate('invalidLayoutUpdate')
+ ]
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate XML layout update
+ *
+ * @param string $xml
+ * @return bool
+ */
+ private function validateXml(string $xml): bool
+ {
+ /** @var $layoutXmlValidator \Magento\Framework\View\Model\Layout\Update\Validator */
+ $layoutXmlValidator = $this->layoutValidatorFactory->create(
+ [
+ 'validationState' => $this->validationState,
+ ]
+ );
+
+ try {
+ if (!$layoutXmlValidator->isValid($xml)) {
+ return false;
+ }
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php
new file mode 100644
index 0000000000000..07d9be4e0a919
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php
@@ -0,0 +1,76 @@
+userContext = $userContext;
+ $this->authorization = $authorization;
+ }
+
+ /**
+ * Validate that the current user is allowed to make design updates
+ *
+ * @param array $data
+ * @return boolean
+ */
+ public function isValid($data): bool
+ {
+ if (empty($data['custom_layout_update'])) {
+ return true;
+ }
+
+ $userType = $this->userContext->getUserType();
+ $isValid = in_array($userType, $this->allowedUserTypes)
+ && $this->authorization->isAllowed('Magento_Catalog::edit_product_design');
+
+ if (!$isValid) {
+ $this->_addMessages(
+ [
+ $this->context->retrieveMessageTemplate('insufficientPermissions'),
+ ]
+ );
+ }
+
+ return $isValid;
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php
new file mode 100644
index 0000000000000..e018fc0cf5ccf
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php
@@ -0,0 +1,101 @@
+userContext = $this->createMock(UserContextInterface::class);
+ $this->authorization = $this->createMock(AuthorizationInterface::class);
+ $this->context = $this->createMock(Product::class);
+ $this->context
+ ->method('retrieveMessageTemplate')
+ ->with('insufficientPermissions')
+ ->willReturn('oh no');
+ $this->validator = new LayoutUpdatePermissions(
+ $this->userContext,
+ $this->authorization
+ );
+ $this->validator->init($this->context);
+ }
+
+ /**
+ * @param $value
+ * @param $userContext
+ * @param $isAllowed
+ * @param $isValid
+ * @dataProvider configurationsProvider
+ */
+ public function testValidationConfiguration($value, $userContext, $isAllowed, $isValid)
+ {
+ $this->userContext
+ ->method('getUserType')
+ ->willReturn($userContext);
+
+ $this->authorization
+ ->method('isAllowed')
+ ->with('Magento_Catalog::edit_product_design')
+ ->willReturn($isAllowed);
+
+ $result = $this->validator->isValid(['custom_layout_update' => $value]);
+ $messages = $this->validator->getMessages();
+
+ self::assertSame($isValid, $result);
+
+ if ($isValid) {
+ self::assertSame([], $messages);
+ } else {
+ self::assertSame(['oh no'], $messages);
+ }
+ }
+
+ public function configurationsProvider()
+ {
+ return [
+ ['', null, null, true],
+ [null, null, null, true],
+ ['foo', UserContextInterface::USER_TYPE_ADMIN, true, true],
+ ['foo', UserContextInterface::USER_TYPE_INTEGRATION, true, true],
+ ['foo', UserContextInterface::USER_TYPE_ADMIN, false, false],
+ ['foo', UserContextInterface::USER_TYPE_INTEGRATION, false, false],
+ ['foo', 'something', null, false],
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php
new file mode 100644
index 0000000000000..d1e8b879f6a08
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php
@@ -0,0 +1,97 @@
+createMock(ValidatorFactory::class);
+ $validationState = $this->createMock(ValidationStateInterface::class);
+ $this->layoutValidator = $this->createMock(Validator::class);
+ $validatorFactory->method('create')
+ ->with(['validationState' => $validationState])
+ ->willReturn($this->layoutValidator);
+
+ $this->validator = new LayoutUpdate(
+ $validatorFactory,
+ $validationState
+ );
+ }
+
+ public function testValidationIsSkippedWithDataNotPresent()
+ {
+ $this->layoutValidator
+ ->expects($this->never())
+ ->method('isValid');
+
+ $result = $this->validator->isValid([]);
+ self::assertTrue($result);
+ }
+
+ public function testValidationFailsProperly()
+ {
+ $this->layoutValidator
+ ->method('isValid')
+ ->with('foo')
+ ->willReturn(false);
+
+ $contextMock = $this->createMock(Product::class);
+ $contextMock
+ ->method('retrieveMessageTemplate')
+ ->with('invalidLayoutUpdate')
+ ->willReturn('oh no');
+ $this->validator->init($contextMock);
+
+ $result = $this->validator->isValid(['custom_layout_update' => 'foo']);
+ $messages = $this->validator->getMessages();
+ self::assertFalse($result);
+ self::assertSame(['oh no'], $messages);
+ }
+
+ public function testInvalidDataException()
+ {
+ $this->layoutValidator
+ ->method('isValid')
+ ->willThrowException(new \Exception('foo'));
+
+ $contextMock = $this->createMock(Product::class);
+ $contextMock
+ ->method('retrieveMessageTemplate')
+ ->with('invalidLayoutUpdate')
+ ->willReturn('oh no');
+ $this->validator->init($contextMock);
+
+ $result = $this->validator->isValid(['custom_layout_update' => 'foo']);
+ $messages = $this->validator->getMessages();
+ self::assertFalse($result);
+ self::assertSame(['oh no'], $messages);
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json
index 180fdb95a4d9b..08515d38687c9 100644
--- a/app/code/Magento/CatalogImportExport/composer.json
+++ b/app/code/Magento/CatalogImportExport/composer.json
@@ -12,6 +12,7 @@
"magento/module-catalog-inventory": "100.2.*",
"magento/module-media-storage": "100.2.*",
"magento/module-customer": "101.0.*",
+ "magento/module-authorization": "*",
"magento/framework": "101.0.*",
"ext-ctype": "*"
},
diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml
index 53772c3b3360a..f51a8ce8bc956 100644
--- a/app/code/Magento/CatalogImportExport/etc/di.xml
+++ b/app/code/Magento/CatalogImportExport/etc/di.xml
@@ -24,7 +24,14 @@
- Magento\CatalogImportExport\Model\Import\Product\Validator\Website
- Magento\CatalogImportExport\Model\Import\Product\Validator\Weight
- Magento\CatalogImportExport\Model\Import\Product\Validator\Quantity
+ - Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate
+ - Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdatePermissions
+
+
+ Magento\Framework\Config\ValidationState\Required
+
+
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php
index 8774d7e69adfe..b2e36c27a413c 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php
@@ -56,6 +56,8 @@ public function __construct(
}
/**
+ * Process the request
+ *
* @return \Magento\Framework\Controller\ResultInterface
* @throws \Magento\Framework\Exception\LocalizedException
*/
@@ -68,10 +70,12 @@ public function execute()
$postItems = $this->getRequest()->getParam('items', []);
if (!($this->getRequest()->getParam('isAjax') && count($postItems))) {
- return $resultJson->setData([
- 'messages' => [__('Please correct the data sent.')],
- 'error' => true,
- ]);
+ return $resultJson->setData(
+ [
+ 'messages' => [__('Please correct the data sent.')],
+ 'error' => true,
+ ]
+ );
}
foreach (array_keys($postItems) as $pageId) {
@@ -98,10 +102,12 @@ public function execute()
}
}
- return $resultJson->setData([
- 'messages' => $messages,
- 'error' => $error
- ]);
+ return $resultJson->setData(
+ [
+ 'messages' => $messages,
+ 'error' => $error
+ ]
+ );
}
/**
@@ -131,7 +137,7 @@ protected function filterPost($postData = [])
*/
protected function validatePost(array $pageData, \Magento\Cms\Model\Page $page, &$error, array &$messages)
{
- if (!($this->dataProcessor->validate($pageData) && $this->dataProcessor->validateRequireEntry($pageData))) {
+ if (!$this->dataProcessor->validateRequireEntry($pageData)) {
$error = true;
foreach ($this->messageManager->getMessages(true)->getItems() as $error) {
$messages[] = $this->getErrorWithPageId($page, $error->getText());
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php
index 9b8933c8dba2e..46f68955531a3 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php
@@ -12,8 +12,7 @@
use Magento\Framework\Config\Dom\ValidationSchemaException;
/**
- * Class PostDataProcessor
- * @package Magento\Cms\Controller\Adminhtml\Page
+ * Processes form data
*/
class PostDataProcessor
{
@@ -80,6 +79,7 @@ public function filter($data)
*
* @param array $data
* @return bool Return FALSE if some item is invalid
+ * @deprecated
*/
public function validate($data)
{
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php
index 42b5c8f8497ec..10f4ed56ccc88 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php
@@ -104,10 +104,6 @@ public function execute()
['page' => $model, 'request' => $this->getRequest()]
);
- if (!$this->dataProcessor->validate($data)) {
- return $resultRedirect->setPath('*/*/edit', ['page_id' => $model->getId(), '_current' => true]);
- }
-
try {
$this->pageRepository->save($model);
$this->messageManager->addSuccessMessage(__('You saved the page.'));
diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php
new file mode 100644
index 0000000000000..9fd94d4c11e1c
--- /dev/null
+++ b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php
@@ -0,0 +1,92 @@
+repository = $repository;
+ $this->validators = $validators;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function save(PageInterface $page)
+ {
+ foreach ($this->validators as $validator) {
+ $validator->validate($page);
+ }
+
+ return $this->repository->save($page);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getById($pageId)
+ {
+ return $this->repository->getById($pageId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getList(SearchCriteriaInterface $searchCriteria)
+ {
+ return $this->repository->getList($searchCriteria);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function delete(PageInterface $page)
+ {
+ return $this->repository->delete($page);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function deleteById($pageId)
+ {
+ return $this->repository->deleteById($pageId);
+ }
+}
diff --git a/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php b/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php
new file mode 100644
index 0000000000000..b4a5da8db73d0
--- /dev/null
+++ b/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php
@@ -0,0 +1,132 @@
+validatorFactory = $validatorFactory;
+ $this->validationState = $validationState;
+ }
+
+ /**
+ * Validate the data before saving
+ *
+ * @param PageInterface $page
+ * @throws LocalizedException
+ */
+ public function validate(PageInterface $page)
+ {
+ $this->validateRequiredFields($page);
+ $this->validateLayoutUpdate($page);
+ $this->validateCustomLayoutUpdate($page);
+ }
+
+ /**
+ * Validate required fields
+ *
+ * @param PageInterface $page
+ * @throws LocalizedException
+ */
+ private function validateRequiredFields(PageInterface $page)
+ {
+ if (empty($page->getTitle())) {
+ throw new LocalizedException(__('Required field "%1" is empty.', 'title'));
+ }
+ }
+
+ /**
+ * Validate layout update
+ *
+ * @param PageInterface $page
+ * @throws LocalizedException
+ */
+ private function validateLayoutUpdate(PageInterface $page)
+ {
+ $layoutXmlValidator = $this->getLayoutValidator();
+
+ try {
+ if (!empty($page->getLayoutUpdateXml())
+ && !$layoutXmlValidator->isValid($page->getLayoutUpdateXml())
+ ) {
+ throw new LocalizedException(__('Layout update is invalid'));
+ }
+ } catch (ValidationException $e) {
+ throw new LocalizedException(__('Layout update is invalid'));
+ } catch (ValidationSchemaException $e) {
+ throw new LocalizedException(__('Layout update is invalid'));
+ }
+ }
+
+ /**
+ * Validate custom layout update
+ *
+ * @param PageInterface $page
+ * @throws LocalizedException
+ */
+ private function validateCustomLayoutUpdate(PageInterface $page)
+ {
+ $layoutXmlValidator = $this->getLayoutValidator();
+
+ try {
+ if (!empty($page->getCustomLayoutUpdateXml())
+ && !$layoutXmlValidator->isValid($page->getCustomLayoutUpdateXml())
+ ) {
+ throw new LocalizedException(__('Custom layout update is invalid'));
+ }
+ } catch (ValidationException $e) {
+ throw new LocalizedException(__('Custom layout update is invalid'));
+ } catch (ValidationSchemaException $e) {
+ throw new LocalizedException(__('Custom layout update is invalid'));
+ }
+ }
+
+ /**
+ * Return a new validator
+ *
+ * @return Validator
+ */
+ private function getLayoutValidator(): Validator
+ {
+ return $this->validatorFactory->create(
+ [
+ 'validationState' => $this->validationState,
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php b/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php
new file mode 100644
index 0000000000000..3a8632c49da64
--- /dev/null
+++ b/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php
@@ -0,0 +1,27 @@
+method('filter')
->with($postData[1])
->willReturnArgument(0);
- $this->dataProcessor->expects($this->once())
- ->method('validate')
- ->with($postData[1])
- ->willReturn(false);
$this->messageManager->expects($this->once())
->method('getMessages')
->with(true)
@@ -122,19 +118,23 @@ public function prepareMocksForTestExecute()
->willReturn('1');
$this->cmsPage->expects($this->atLeastOnce())
->method('getData')
- ->willReturn([
- 'layout' => '1column',
- 'identifier' => 'test-identifier'
- ]);
+ ->willReturn(
+ [
+ 'layout' => '1column',
+ 'identifier' => 'test-identifier'
+ ]
+ );
$this->cmsPage->expects($this->once())
->method('setData')
- ->with([
- 'layout' => '1column',
- 'title' => '404 Not Found',
- 'identifier' => 'no-route',
- 'custom_theme' => '1',
- 'custom_root_template' => '2'
- ]);
+ ->with(
+ [
+ 'layout' => '1column',
+ 'title' => '404 Not Found',
+ 'identifier' => 'no-route',
+ 'custom_theme' => '1',
+ 'custom_root_template' => '2'
+ ]
+ );
$this->jsonFactory->expects($this->once())
->method('create')
->willReturn($this->resultJson);
@@ -149,13 +149,15 @@ public function testExecuteWithLocalizedException()
->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('LocalizedException')));
$this->resultJson->expects($this->once())
->method('setData')
- ->with([
- 'messages' => [
- '[Page ID: 1] Error message',
- '[Page ID: 1] LocalizedException'
- ],
- 'error' => true
- ])
+ ->with(
+ [
+ 'messages' => [
+ '[Page ID: 1] Error message',
+ '[Page ID: 1] LocalizedException'
+ ],
+ 'error' => true
+ ]
+ )
->willReturnSelf();
$this->assertSame($this->resultJson, $this->controller->execute());
@@ -170,13 +172,15 @@ public function testExecuteWithRuntimeException()
->willThrowException(new \RuntimeException(__('RuntimeException')));
$this->resultJson->expects($this->once())
->method('setData')
- ->with([
- 'messages' => [
- '[Page ID: 1] Error message',
- '[Page ID: 1] RuntimeException'
- ],
- 'error' => true
- ])
+ ->with(
+ [
+ 'messages' => [
+ '[Page ID: 1] Error message',
+ '[Page ID: 1] RuntimeException'
+ ],
+ 'error' => true
+ ]
+ )
->willReturnSelf();
$this->assertSame($this->resultJson, $this->controller->execute());
@@ -191,13 +195,15 @@ public function testExecuteWithException()
->willThrowException(new \Exception(__('Exception')));
$this->resultJson->expects($this->once())
->method('setData')
- ->with([
- 'messages' => [
- '[Page ID: 1] Error message',
- '[Page ID: 1] Something went wrong while saving the page.'
- ],
- 'error' => true
- ])
+ ->with(
+ [
+ 'messages' => [
+ '[Page ID: 1] Error message',
+ '[Page ID: 1] Something went wrong while saving the page.'
+ ],
+ 'error' => true
+ ]
+ )
->willReturnSelf();
$this->assertSame($this->resultJson, $this->controller->execute());
@@ -218,12 +224,14 @@ public function testExecuteWithoutData()
);
$this->resultJson->expects($this->once())
->method('setData')
- ->with([
- 'messages' => [
- 'Please correct the data sent.'
- ],
- 'error' => true
- ])
+ ->with(
+ [
+ 'messages' => [
+ 'Please correct the data sent.'
+ ],
+ 'error' => true
+ ]
+ )
->willReturnSelf();
$this->assertSame($this->resultJson, $this->controller->execute());
diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php
new file mode 100644
index 0000000000000..f73396230a669
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php
@@ -0,0 +1,145 @@
+subject = $this->createMock(PageRepositoryInterface::class);
+ }
+
+ /**
+ * @param $validators
+ * @expectedException \InvalidArgumentException
+ * @dataProvider constructorArgumentProvider
+ */
+ public function testConstructorValidation($validators)
+ {
+ new ValidationComposite($this->subject, $validators);
+ }
+
+ public function testSaveInvokesValidatorsWithSucess()
+ {
+ $validator1 = $this->createMock(ValidatorInterface::class);
+ $validator2 = $this->createMock(ValidatorInterface::class);
+ $page = $this->createMock(PageInterface::class);
+
+ // Assert each are called
+ $validator1
+ ->expects($this->once())
+ ->method('validate')
+ ->with($page);
+ $validator2
+ ->expects($this->once())
+ ->method('validate')
+ ->with($page);
+
+ // Assert that the success is called
+ $this->subject
+ ->expects($this->once())
+ ->method('save')
+ ->with($page)
+ ->willReturn('foo');
+
+ $composite = new ValidationComposite($this->subject, [$validator1, $validator2]);
+ $result = $composite->save($page);
+
+ self::assertSame('foo', $result);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage Oh no. That isn't right.
+ */
+ public function testSaveInvokesValidatorsWithErrors()
+ {
+ $validator1 = $this->createMock(ValidatorInterface::class);
+ $validator2 = $this->createMock(ValidatorInterface::class);
+ $page = $this->createMock(PageInterface::class);
+
+ // Assert the first is called
+ $validator1
+ ->expects($this->once())
+ ->method('validate')
+ ->with($page)
+ ->willThrowException(new LocalizedException(__('Oh no. That isn\'t right.')));
+
+ // Assert the second is NOT called
+ $validator2
+ ->expects($this->never())
+ ->method('validate');
+
+ // Assert that the success is NOT called
+ $this->subject
+ ->expects($this->never())
+ ->method('save');
+
+ $composite = new ValidationComposite($this->subject, [$validator1, $validator2]);
+ $composite->save($page);
+ }
+
+ /**
+ * @param $method
+ * @param $arg
+ * @dataProvider passthroughMethodDataProvider
+ */
+ public function testPassthroughMethods($method, $arg)
+ {
+ $this->subject
+ ->method($method)
+ ->with($arg)
+ ->willReturn('foo');
+
+ $composite = new ValidationComposite($this->subject, []);
+ $result = $composite->{$method}($arg);
+
+ self::assertSame('foo', $result);
+ }
+
+ public function constructorArgumentProvider()
+ {
+ return [
+ [[null], false],
+ [[''], false],
+ [['foo'], false],
+ [[new \stdClass()], false],
+ [[$this->createMock(ValidatorInterface::class), 'foo'], false],
+ ];
+ }
+
+ public function passthroughMethodDataProvider()
+ {
+ return [
+ ['save', $this->createMock(PageInterface::class)],
+ ['getById', 1],
+ ['getList', $this->createMock(SearchCriteriaInterface::class)],
+ ['delete', $this->createMock(PageInterface::class)],
+ ['deleteById', 1],
+ ];
+ }
+}
diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php
new file mode 100644
index 0000000000000..487a90bb9a185
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php
@@ -0,0 +1,124 @@
+createMock(ValidatorFactory::class);
+ $this->layoutValidator = $this->createMock(Validator::class);
+ $layoutValidatorState = $this->createMock(ValidationStateInterface::class);
+
+ $layoutValidatorFactory
+ ->method('create')
+ ->with(['validationState' => $layoutValidatorState])
+ ->willReturn($this->layoutValidator);
+
+ $this->validator = new LayoutUpdateValidator($layoutValidatorFactory, $layoutValidatorState);
+ }
+
+ /**
+ * @dataProvider validationSetDataProvider
+ */
+ public function testValidate($data, $expectedExceptionMessage, $layoutValidatorException, $isLayoutValid = false)
+ {
+ if ($expectedExceptionMessage) {
+ $this->expectException(LocalizedException::class);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+ }
+
+ if ($layoutValidatorException) {
+ $this->layoutValidator
+ ->method('isValid')
+ ->with($data['getLayoutUpdateXml'] ?? $data['getCustomLayoutUpdateXml'])
+ ->willThrowException($layoutValidatorException);
+ } elseif (!empty($data['getLayoutUpdateXml'])) {
+ $this->layoutValidator
+ ->method('isValid')
+ ->with($data['getLayoutUpdateXml'])
+ ->willReturn($isLayoutValid);
+ } elseif (!empty($data['getCustomLayoutUpdateXml'])) {
+ $this->layoutValidator
+ ->method('isValid')
+ ->with($data['getCustomLayoutUpdateXml'])
+ ->willReturn($isLayoutValid);
+ }
+
+ $page = $this->createMock(PageInterface::class);
+ foreach ($data as $method => $value) {
+ $page
+ ->method($method)
+ ->willReturn($value);
+ }
+
+ self::assertNull($this->validator->validate($page));
+ }
+
+ public function validationSetDataProvider()
+ {
+ $layoutError = 'Layout update is invalid';
+ $customLayoutError = 'Custom layout update is invalid';
+ $validationException = new ValidationException('Invalid format');
+ $schemaException = new ValidationSchemaException(__('Invalid format'));
+
+ return [
+ [['getTitle' => ''], 'Required field "title" is empty.', null],
+ [['getTitle' => null], 'Required field "title" is empty.', null],
+ [['getTitle' => false], 'Required field "title" is empty.', null],
+ [['getTitle' => 0], 'Required field "title" is empty.', null],
+ [['getTitle' => '0'], 'Required field "title" is empty.', null],
+ [['getTitle' => []], 'Required field "title" is empty.', null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => ''], null, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => null], null, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => false], null, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => 0], null, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => '0'], null, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => []], null, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, null],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, $validationException],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, $schemaException],
+ [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], null, null, true],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => ''], null, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => null], null, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => false], null, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 0], null, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => '0'], null, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => []], null, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, null],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, $validationException],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, $schemaException],
+ [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], null, null, true],
+ ];
+ }
+}
diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml
index b883e8f93f752..e610a4a5e3cf1 100644
--- a/app/code/Magento/Cms/etc/di.xml
+++ b/app/code/Magento/Cms/etc/di.xml
@@ -13,7 +13,7 @@
-
+
@@ -229,4 +229,13 @@
+
+
+
+ Magento\Cms\Model\PageRepository
+
+ - Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator
+
+
+
diff --git a/app/code/Magento/Config/Model/Config/Source/Email/Template.php b/app/code/Magento/Config/Model/Config/Source/Email/Template.php
index 04222733418d3..c6b28cd7c46a9 100644
--- a/app/code/Magento/Config/Model/Config/Source/Email/Template.php
+++ b/app/code/Magento/Config/Model/Config/Source/Email/Template.php
@@ -62,6 +62,12 @@ public function toOptionArray()
$templateLabel = $this->_emailConfig->getTemplateLabel($templateId);
$templateLabel = __('%1 (Default)', $templateLabel);
array_unshift($options, ['value' => $templateId, 'label' => $templateLabel]);
+ array_walk(
+ $options,
+ function (&$item) {
+ $item['__disableTmpl'] = true;
+ }
+ );
return $options;
}
}
diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php
index e869fe8556bf7..b1a472717b785 100644
--- a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php
+++ b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php
@@ -76,9 +76,21 @@ public function testToOptionArray()
$this->returnValue('Template New')
);
$expectedResult = [
- ['value' => 'template_new', 'label' => 'Template New (Default)'],
- ['value' => 'template_one', 'label' => 'Template One'],
- ['value' => 'template_two', 'label' => 'Template Two'],
+ [
+ 'value' => 'template_new',
+ 'label' => 'Template New (Default)',
+ '__disableTmpl' => true
+ ],
+ [
+ 'value' => 'template_one',
+ 'label' => 'Template One',
+ '__disableTmpl' => true
+ ],
+ [
+ 'value' => 'template_two',
+ 'label' => 'Template Two',
+ '__disableTmpl' => true
+ ],
];
$this->_model->setPath('template/new');
$this->assertEquals($expectedResult, $this->_model->toOptionArray());
diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php
index 538bf34fca6d3..ac9ca65b17115 100644
--- a/app/code/Magento/Customer/Model/Customer/DataProvider.php
+++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php
@@ -362,7 +362,14 @@ protected function getAttributesMeta(Type $entityType)
$meta[$code]['arguments']['data']['config']['options'] = $this->getCountryWithWebsiteSource()
->getAllOptions();
} else {
- $meta[$code]['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions();
+ $options = $attribute->getSource()->getAllOptions();
+ array_walk(
+ $options,
+ function (&$item) {
+ $item['__disableTmpl'] = ['label' => true];
+ }
+ );
+ $meta[$code]['arguments']['data']['config']['options'] = $options;
}
}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php
index cf247f1832862..e7be01c9c5c0f 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php
@@ -22,11 +22,21 @@
* Test for class \Magento\Customer\Model\Customer\DataProvider
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
class DataProviderTest extends \PHPUnit\Framework\TestCase
{
const ATTRIBUTE_CODE = 'test-code';
- const OPTIONS_RESULT = 'test-options';
+ const OPTIONS_RESULT = [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1'
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2'
+ ],
+ ];
/**
* @var Config|\PHPUnit_Framework_MockObject_MockObject
@@ -145,8 +155,19 @@ public function getAttributesMetaDataProvider()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
- 'visible' => null,
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ '__disableTmpl' => ['label' => true],
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ '__disableTmpl' => ['label' => true],
+ ],
+ ],
+ 'visible' => false,
'required' => 'is_required',
'label' => __('frontend_label'),
'sortOrder' => 'sort_order',
@@ -165,7 +186,7 @@ public function getAttributesMetaDataProvider()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'visible' => null,
+ 'visible' => false,
'required' => 'is_required',
'label' => __('frontend_label'),
'sortOrder' => 'sort_order',
@@ -193,8 +214,19 @@ public function getAttributesMetaDataProvider()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
- 'visible' => null,
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ '__disableTmpl' => ['label' => true],
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ '__disableTmpl' => ['label' => true],
+ ],
+ ],
+ 'visible' => false,
'required' => 'is_required',
'label' => __('frontend_label'),
'sortOrder' => 'sort_order',
@@ -213,7 +245,7 @@ public function getAttributesMetaDataProvider()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'visible' => null,
+ 'visible' => false,
'required' => 'is_required',
'label' => 'frontend_label',
'sortOrder' => 'sort_order',
@@ -237,8 +269,17 @@ public function getAttributesMetaDataProvider()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
- 'visible' => null,
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ ],
+ ],
+ 'visible' => false,
'required' => 'is_required',
'label' => __('frontend_label'),
'sortOrder' => 'sort_order',
@@ -528,7 +569,7 @@ private function getCountryAttrMock()
->getMock();
$countryByWebsiteMock->expects($this->any())
->method('getAllOptions')
- ->willReturn('test-options');
+ ->willReturn(self::OPTIONS_RESULT);
$shareMock = $this->getMockBuilder(Share::class)
->disableOriginalConstructor()
->getMock();
@@ -1386,7 +1427,18 @@ private function getCustomerAttributeExpectations()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ '__disableTmpl' => ['label' => true],
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ '__disableTmpl' => ['label' => true],
+ ],
+ ],
'visible' => true,
'required' => 'is_required',
'label' => __('frontend_label'),
@@ -1406,7 +1458,18 @@ private function getCustomerAttributeExpectations()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ '__disableTmpl' => ['label' => true],
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ '__disableTmpl' => ['label' => true],
+ ],
+ ],
'visible' => true,
'required' => 'is_required',
'label' => __('frontend_label'),
@@ -1490,8 +1553,19 @@ private function getExpectationForVisibleAttributes()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
- 'visible' => null,
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ '__disableTmpl' => ['label' => true],
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ '__disableTmpl' => ['label' => true],
+ ],
+ ],
+ 'visible' => false,
'required' => 'is_required',
'label' => __('frontend_label'),
'sortOrder' => 'sort_order',
@@ -1510,7 +1584,7 @@ private function getExpectationForVisibleAttributes()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'visible' => null,
+ 'visible' => false,
'required' => 'is_required',
'label' => 'frontend_label',
'sortOrder' => 'sort_order',
@@ -1534,8 +1608,17 @@ private function getExpectationForVisibleAttributes()
'config' => [
'dataType' => 'frontend_input',
'formElement' => 'frontend_input',
- 'options' => 'test-options',
- 'visible' => null,
+ 'options' => [
+ [
+ 'label' => 'label-1',
+ 'value' => 'value-1',
+ ],
+ [
+ 'label' => 'label-2',
+ 'value' => 'value-2',
+ ],
+ ],
+ 'visible' => false,
'required' => 'is_required',
'label' => __('frontend_label'),
'sortOrder' => 'sort_order',
diff --git a/app/etc/di.xml b/app/etc/di.xml
index 1efe5a5ececd8..d6ba195640fda 100755
--- a/app/etc/di.xml
+++ b/app/etc/di.xml
@@ -637,6 +637,16 @@
developerPublisher
+
+
+ true
+
+
+
+
+ false
+
+
Magento\Framework\View\Asset\PreProcessor\MinificationFilenameResolver
diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php
index cb96910ec86e1..5e48129a586a9 100644
--- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php
+++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php
@@ -106,6 +106,7 @@ protected function tearDown()
try {
$product = $productRepository->get($productSku, false, null, true);
$productRepository->delete($product);
+ // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch
} catch (NoSuchEntityException $e) {
// nothing to delete
}
@@ -2294,6 +2295,8 @@ public function testImportImageForNonDefaultStore()
*/
public function testProductsWithMultipleStoresWhenMediaIsDisabled()
{
+ $this->loginAdminUserWithUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME);
+
$filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class);
$directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
$source = $this->objectManager->create(
@@ -2531,4 +2534,22 @@ public function testImportProductWithContinueOnError()
$stockItem = $stockRegistry->getStockItem($product->getId());
$this->assertEquals(17, $stockItem->getQty());
}
+
+ /**
+ * Set the current admin session user based on a username
+ *
+ * @param string $username
+ */
+ private function loginAdminUserWithUsername(string $username)
+ {
+ $user = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
+ \Magento\User\Model\User::class
+ )->loadByUsername($username);
+
+ /** @var $session \Magento\Backend\Model\Auth\Session */
+ $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(
+ \Magento\Backend\Model\Auth\Session::class
+ );
+ $session->setUser($user);
+ }
}
diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/ValidationState/ConfigurableTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/ValidationState/ConfigurableTest.php
new file mode 100644
index 0000000000000..cbd9e43632489
--- /dev/null
+++ b/lib/internal/Magento/Framework/Config/Test/Unit/ValidationState/ConfigurableTest.php
@@ -0,0 +1,30 @@
+isValidationRequired());
+ }
+
+ public function testFalse()
+ {
+ $state = new Configurable(false);
+ self::assertFalse($state->isValidationRequired());
+ }
+}
diff --git a/lib/internal/Magento/Framework/Config/ValidationState/Configurable.php b/lib/internal/Magento/Framework/Config/ValidationState/Configurable.php
new file mode 100644
index 0000000000000..c996b2a3e135d
--- /dev/null
+++ b/lib/internal/Magento/Framework/Config/ValidationState/Configurable.php
@@ -0,0 +1,38 @@
+required = $required;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isValidationRequired(): bool
+ {
+ return $this->required;
+ }
+}