Skip to content

Commit 4539cb0

Browse files
Merge pull request #6233 from magento-qwerty/MC-34385
[CIA] Filter fields allowing HTML
2 parents c03bde6 + e22ac0a commit 4539cb0

File tree

22 files changed

+1505
-22
lines changed

22 files changed

+1505
-22
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Catalog\Model\Attribute\Backend;
10+
11+
use Magento\Catalog\Model\AbstractModel;
12+
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
13+
use Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend as ParentBackend;
14+
use Magento\Eav\Model\Entity\Attribute\Exception;
15+
use Magento\Framework\DataObject;
16+
use Magento\Framework\Exception\LocalizedException;
17+
use Magento\Framework\Validation\ValidationException;
18+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
19+
20+
/**
21+
* Default backend model for catalog attributes.
22+
*/
23+
class DefaultBackend extends ParentBackend
24+
{
25+
/**
26+
* @var WYSIWYGValidatorInterface
27+
*/
28+
private $wysiwygValidator;
29+
30+
/**
31+
* @param WYSIWYGValidatorInterface $wysiwygValidator
32+
*/
33+
public function __construct(WYSIWYGValidatorInterface $wysiwygValidator)
34+
{
35+
$this->wysiwygValidator = $wysiwygValidator;
36+
}
37+
38+
/**
39+
* Validate user HTML value.
40+
*
41+
* @param DataObject $object
42+
* @return void
43+
* @throws LocalizedException
44+
*/
45+
private function validateHtml(DataObject $object): void
46+
{
47+
$attribute = $this->getAttribute();
48+
$code = $attribute->getAttributeCode();
49+
if ($attribute instanceof Attribute && $attribute->getIsHtmlAllowedOnFront()) {
50+
$value = $object->getData($code);
51+
if ($value
52+
&& is_string($value)
53+
&& (!($object instanceof AbstractModel) || $object->getData($code) !== $object->getOrigData($code))
54+
) {
55+
try {
56+
$this->wysiwygValidator->validate($object->getData($code));
57+
} catch (ValidationException $exception) {
58+
$attributeException = new Exception(
59+
__(
60+
'Using restricted HTML elements for "%1". %2',
61+
$attribute->getName(),
62+
$exception->getMessage()
63+
),
64+
$exception
65+
);
66+
$attributeException->setAttributeCode($code)->setPart('backend');
67+
throw $attributeException;
68+
}
69+
}
70+
}
71+
}
72+
73+
/**
74+
* @inheritDoc
75+
*/
76+
public function beforeSave($object)
77+
{
78+
parent::beforeSave($object);
79+
$this->validateHtml($object);
80+
81+
return $this;
82+
}
83+
84+
/**
85+
* @inheritDoc
86+
*/
87+
public function validate($object)
88+
{
89+
$isValid = parent::validate($object);
90+
if ($isValid) {
91+
$this->validateHtml($object);
92+
}
93+
94+
return $isValid;
95+
}
96+
}

app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
namespace Magento\Catalog\Model\ResourceModel\Eav;
88

9+
use Magento\Catalog\Model\Attribute\Backend\DefaultBackend;
910
use Magento\Catalog\Model\Attribute\LockValidatorInterface;
11+
use Magento\Eav\Model\Entity;
1012
use Magento\Framework\Api\AttributeValueFactory;
1113
use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface;
1214

@@ -902,4 +904,17 @@ public function setIsFilterableInGrid($isFilterableInGrid)
902904
$this->setData(self::IS_FILTERABLE_IN_GRID, $isFilterableInGrid);
903905
return $this;
904906
}
907+
908+
/**
909+
* @inheritDoc
910+
*/
911+
protected function _getDefaultBackendModel()
912+
{
913+
$backend = parent::_getDefaultBackendModel();
914+
if ($backend === Entity::DEFAULT_BACKEND_MODEL) {
915+
$backend = DefaultBackend::class;
916+
}
917+
918+
return $backend;
919+
}
905920
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend;
10+
11+
use Magento\Catalog\Model\AbstractModel;
12+
use Magento\Catalog\Model\Attribute\Backend\DefaultBackend;
13+
use Magento\Framework\DataObject;
14+
use Magento\Framework\Validation\ValidationException;
15+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
16+
use PHPUnit\Framework\TestCase;
17+
use Magento\Eav\Model\Entity\Attribute as BasicAttribute;
18+
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
19+
use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException;
20+
21+
class DefaultBackendTest extends TestCase
22+
{
23+
/**
24+
* Different cases for attribute validation.
25+
*
26+
* @return array
27+
*/
28+
public function getAttributeConfigurations(): array
29+
{
30+
return [
31+
'basic-attribute' => [true, false, true, 'basic', 'value', false, true, false],
32+
'non-html-attribute' => [false, false, false, 'non-html', 'value', false, false, false],
33+
'empty-html-attribute' => [false, false, true, 'html', null, false, true, false],
34+
'invalid-html-attribute' => [false, false, false, 'html', 'value', false, true, true],
35+
'valid-html-attribute' => [false, true, false, 'html', 'value', false, true, false],
36+
'changed-invalid-html-attribute' => [false, false, true, 'html', 'value', true, true, true],
37+
'changed-valid-html-attribute' => [false, true, true, 'html', 'value', true, true, false]
38+
];
39+
}
40+
41+
/**
42+
* Test attribute validation.
43+
*
44+
* @param bool $isBasic
45+
* @param bool $isValidated
46+
* @param bool $isCatalogEntity
47+
* @param string $code
48+
* @param mixed $value
49+
* @param bool $isChanged
50+
* @param bool $isHtmlAttribute
51+
* @param bool $exceptionThrown
52+
* @dataProvider getAttributeConfigurations
53+
*/
54+
public function testValidate(
55+
bool $isBasic,
56+
bool $isValidated,
57+
bool $isCatalogEntity,
58+
string $code,
59+
$value,
60+
bool $isChanged,
61+
bool $isHtmlAttribute,
62+
bool $exceptionThrown
63+
): void {
64+
if ($isBasic) {
65+
$attributeMock = $this->createMock(BasicAttribute::class);
66+
} else {
67+
$attributeMock = $this->createMock(Attribute::class);
68+
$attributeMock->expects($this->any())
69+
->method('getIsHtmlAllowedOnFront')
70+
->willReturn($isHtmlAttribute);
71+
}
72+
$attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($code);
73+
74+
$validatorMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class);
75+
if (!$isValidated) {
76+
$validatorMock->expects($this->any())
77+
->method('validate')
78+
->willThrowException(new ValidationException(__('HTML is invalid')));
79+
} else {
80+
$validatorMock->expects($this->any())->method('validate');
81+
}
82+
83+
if ($isCatalogEntity) {
84+
$objectMock = $this->createMock(AbstractModel::class);
85+
$objectMock->expects($this->any())
86+
->method('getOrigData')
87+
->willReturn($isChanged ? $value .'-OLD' : $value);
88+
} else {
89+
$objectMock = $this->createMock(DataObject::class);
90+
}
91+
$objectMock->expects($this->any())->method('getData')->with($code)->willReturn($value);
92+
93+
$model = new DefaultBackend($validatorMock);
94+
$model->setAttribute($attributeMock);
95+
96+
$actuallyThrownForSave = false;
97+
try {
98+
$model->beforeSave($objectMock);
99+
} catch (AttributeException $exception) {
100+
$actuallyThrownForSave = true;
101+
}
102+
$actuallyThrownForValidate = false;
103+
try {
104+
$model->validate($objectMock);
105+
} catch (AttributeException $exception) {
106+
$actuallyThrownForValidate = true;
107+
}
108+
$this->assertEquals($actuallyThrownForSave, $actuallyThrownForValidate);
109+
$this->assertEquals($actuallyThrownForSave, $exceptionThrown);
110+
}
111+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Cms\Command;
10+
11+
use Magento\Cms\Model\Wysiwyg\Validator;
12+
use Symfony\Component\Console\Command\Command;
13+
use Symfony\Component\Console\Input\InputArgument;
14+
use Symfony\Component\Console\Input\InputInterface;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
use Magento\Framework\App\Config\ConfigResource\ConfigInterface as ConfigWriter;
17+
use Magento\Framework\App\Cache\TypeListInterface as Cache;
18+
19+
/**
20+
* Command to toggle WYSIWYG content validation on/off.
21+
*/
22+
class WysiwygRestrictCommand extends Command
23+
{
24+
/**
25+
* @var ConfigWriter
26+
*/
27+
private $configWriter;
28+
29+
/**
30+
* @var Cache
31+
*/
32+
private $cache;
33+
34+
/**
35+
* @param ConfigWriter $configWriter
36+
* @param Cache $cache
37+
*/
38+
public function __construct(ConfigWriter $configWriter, Cache $cache)
39+
{
40+
parent::__construct();
41+
42+
$this->configWriter = $configWriter;
43+
$this->cache = $cache;
44+
}
45+
46+
/**
47+
* @inheritDoc
48+
*/
49+
protected function configure()
50+
{
51+
$this->setName('cms:wysiwyg:restrict');
52+
$this->setDescription('Set whether to enforce user HTML content validation or show a warning instead');
53+
$this->setDefinition([new InputArgument('restrict', InputArgument::REQUIRED, 'y\n')]);
54+
55+
parent::configure();
56+
}
57+
58+
/**
59+
* @inheritDoc
60+
*/
61+
protected function execute(InputInterface $input, OutputInterface $output)
62+
{
63+
$restrictArg = mb_strtolower((string)$input->getArgument('restrict'));
64+
$restrict = $restrictArg === 'y' ? '1' : '0';
65+
$this->configWriter->saveConfig(Validator::CONFIG_PATH_THROW_EXCEPTION, $restrict);
66+
$this->cache->cleanType('config');
67+
68+
$output->writeln('HTML user content validation is now ' .($restrictArg === 'y' ? 'enforced' : 'suggested'));
69+
70+
return 0;
71+
}
72+
}

app/code/Magento/Cms/Model/Block.php

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
namespace Magento\Cms\Model;
77

88
use Magento\Cms\Api\Data\BlockInterface;
9+
use Magento\Framework\App\ObjectManager;
910
use Magento\Framework\DataObject\IdentityInterface;
1011
use Magento\Framework\Model\AbstractModel;
12+
use Magento\Framework\Validation\ValidationException;
13+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
14+
use Magento\Framework\Model\Context;
15+
use Magento\Framework\Registry;
16+
use Magento\Framework\Model\ResourceModel\AbstractResource;
17+
use Magento\Framework\Data\Collection\AbstractDb;
1118

1219
/**
1320
* CMS block model
@@ -40,6 +47,32 @@ class Block extends AbstractModel implements BlockInterface, IdentityInterface
4047
*/
4148
protected $_eventPrefix = 'cms_block';
4249

50+
/**
51+
* @var WYSIWYGValidatorInterface
52+
*/
53+
private $wysiwygValidator;
54+
55+
/**
56+
* @param Context $context
57+
* @param Registry $registry
58+
* @param AbstractResource|null $resource
59+
* @param AbstractDb|null $resourceCollection
60+
* @param array $data
61+
* @param WYSIWYGValidatorInterface|null $wysiwygValidator
62+
*/
63+
public function __construct(
64+
Context $context,
65+
Registry $registry,
66+
AbstractResource $resource = null,
67+
AbstractDb $resourceCollection = null,
68+
array $data = [],
69+
?WYSIWYGValidatorInterface $wysiwygValidator = null
70+
) {
71+
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
72+
$this->wysiwygValidator = $wysiwygValidator
73+
?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class);
74+
}
75+
4376
/**
4477
* Construct.
4578
*
@@ -63,12 +96,26 @@ public function beforeSave()
6396
}
6497

6598
$needle = 'block_id="' . $this->getId() . '"';
66-
if (false == strstr($this->getContent(), (string) $needle)) {
67-
return parent::beforeSave();
99+
if (strstr($this->getContent(), (string) $needle) !== false) {
100+
throw new \Magento\Framework\Exception\LocalizedException(
101+
__('Make sure that static block content does not reference the block itself.')
102+
);
68103
}
69-
throw new \Magento\Framework\Exception\LocalizedException(
70-
__('Make sure that static block content does not reference the block itself.')
71-
);
104+
parent::beforeSave();
105+
106+
//Validating HTML content.
107+
if ($this->getContent() && $this->getContent() !== $this->getOrigData(self::CONTENT)) {
108+
try {
109+
$this->wysiwygValidator->validate($this->getContent());
110+
} catch (ValidationException $exception) {
111+
throw new ValidationException(
112+
__('Content field contains restricted HTML elements. %1', $exception->getMessage()),
113+
$exception
114+
);
115+
}
116+
}
117+
118+
return $this;
72119
}
73120

74121
/**

0 commit comments

Comments
 (0)