Skip to content

Commit 6b5bb10

Browse files
author
ogorkun
committed
MC-34385: Filter fields allowing HTML
1 parent c512c9d commit 6b5bb10

File tree

6 files changed

+320
-0
lines changed

6 files changed

+320
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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\Model\Wysiwyg;
10+
11+
use Magento\Framework\App\Config\ScopeConfigInterface;
12+
use Magento\Framework\Message\ManagerInterface;
13+
use Magento\Framework\Validation\ValidationException;
14+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
15+
use Psr\Log\LoggerInterface;
16+
17+
/**
18+
* Processes backend validator results.
19+
*/
20+
class Validator implements WYSIWYGValidatorInterface
21+
{
22+
public const CONFIG_PATH_THROW_EXCEPTION = 'cms/wysiwyg/force_valid';
23+
24+
/**
25+
* @var WYSIWYGValidatorInterface
26+
*/
27+
private $validator;
28+
29+
/**
30+
* @var ManagerInterface
31+
*/
32+
private $messages;
33+
34+
/**
35+
* @var ScopeConfigInterface
36+
*/
37+
private $config;
38+
39+
/**
40+
* @var LoggerInterface
41+
*/
42+
private $logger;
43+
44+
/**
45+
* @param WYSIWYGValidatorInterface $validator
46+
* @param ManagerInterface $messages
47+
* @param ScopeConfigInterface $config
48+
* @param LoggerInterface $logger
49+
*/
50+
public function __construct(
51+
WYSIWYGValidatorInterface $validator,
52+
ManagerInterface $messages,
53+
ScopeConfigInterface $config,
54+
LoggerInterface $logger
55+
) {
56+
$this->validator = $validator;
57+
$this->messages = $messages;
58+
$this->config = $config;
59+
$this->logger = $logger;
60+
}
61+
62+
/**
63+
* @inheritDoc
64+
*/
65+
public function validate(string $content): void
66+
{
67+
$throwException = $this->config->isSetFlag(self::CONFIG_PATH_THROW_EXCEPTION);
68+
try {
69+
$this->validator->validate($content);
70+
} catch (ValidationException $exception) {
71+
if ($throwException) {
72+
throw $exception;
73+
} else {
74+
$this->messages->addWarningMessage(
75+
__('Temporarily allowed to save restricted HTML value. %1', $exception->getMessage())
76+
);
77+
}
78+
} catch (\Throwable $exception) {
79+
if ($throwException) {
80+
throw $exception;
81+
} else {
82+
$this->messages->addWarningMessage(__('Invalid HTML provided')->render());
83+
$this->logger->error($exception);
84+
}
85+
}
86+
}
87+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Test\Unit\Model\Wysiwyg;
10+
11+
use Magento\Cms\Model\Wysiwyg\Validator;
12+
use Magento\Framework\App\Config\ScopeConfigInterface;
13+
use Magento\Framework\Message\ManagerInterface;
14+
use Magento\Framework\Validation\ValidationException;
15+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
16+
use PHPUnit\Framework\TestCase;
17+
use Psr\Log\LoggerInterface;
18+
19+
class ValidatorTest extends TestCase
20+
{
21+
/**
22+
* Validation cases.
23+
*
24+
* @return array
25+
*/
26+
public function getValidationCases(): array
27+
{
28+
return [
29+
'invalid-exception' => [true, new ValidationException(__('Invalid html')), true, false],
30+
'invalid-warning' => [false, new \RuntimeException('Invalid html'), false, true],
31+
'valid' => [false, null, false, false]
32+
];
33+
}
34+
35+
/**
36+
* Test validation.
37+
*
38+
* @param bool $isFlagSet
39+
* @param \Throwable|null $thrown
40+
* @param bool $exceptionThrown
41+
* @param bool $warned
42+
* @dataProvider getValidationCases
43+
*/
44+
public function testValidate(bool $isFlagSet, ?\Throwable $thrown, bool $exceptionThrown, bool $warned): void
45+
{
46+
$actuallyWarned = false;
47+
48+
$configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class);
49+
$configMock->method('isSetFlag')
50+
->with(Validator::CONFIG_PATH_THROW_EXCEPTION)
51+
->willReturn($isFlagSet);
52+
53+
$backendMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class);
54+
if ($thrown) {
55+
$backendMock->method('validate')->willThrowException($thrown);
56+
}
57+
58+
$messagesMock = $this->getMockForAbstractClass(ManagerInterface::class);
59+
$messagesMock->method('addWarningMessage')
60+
->willReturnCallback(
61+
function () use (&$actuallyWarned): void {
62+
$actuallyWarned = true;
63+
}
64+
);
65+
66+
$loggerMock = $this->getMockForAbstractClass(LoggerInterface::class);
67+
68+
$validator = new Validator($backendMock, $messagesMock, $configMock, $loggerMock);
69+
try {
70+
$validator->validate('content');
71+
$actuallyThrown = false;
72+
} catch (\Throwable $exception) {
73+
$actuallyThrown = true;
74+
}
75+
$this->assertEquals($exceptionThrown, $actuallyThrown);
76+
$this->assertEquals($warned, $actuallyWarned);
77+
}
78+
}

app/code/Magento/Cms/etc/config.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<wysiwyg>
2525
<enabled>enabled</enabled>
2626
<editor>mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter</editor>
27+
<force_valid>0</force_valid>
2728
</wysiwyg>
2829
</cms>
2930
<system>

app/code/Magento/Cms/etc/di.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,10 @@
243243
</arguments>
244244
</type>
245245
<preference for="Magento\Cms\Model\Page\CustomLayoutRepositoryInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutRepository" />
246+
<type name="Magento\Cms\Model\Wysiwyg\Validator">
247+
<arguments>
248+
<argument name="validator" xsi:type="object">DefaultWYSIWYGValidator</argument>
249+
</arguments>
250+
</type>
251+
<preference for="Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface" type="Magento\Cms\Model\Wysiwyg\Validator" />
246252
</config>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 Magento\Framework\App\Config\ReinitableConfigInterface;
13+
use Magento\TestFramework\Helper\Bootstrap;
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Tester\CommandTester;
16+
17+
/**
18+
* Test the command.
19+
*
20+
* @magentoAppIsolation enabled
21+
* @magentoDbIsolation enabled
22+
*/
23+
class WysiwygRestrictCommandTest extends TestCase
24+
{
25+
/**
26+
* @var ReinitableConfigInterface
27+
*/
28+
private $config;
29+
30+
/**
31+
* @var WysiwygRestrictCommandFactory
32+
*/
33+
private $factory;
34+
35+
/**
36+
* @inheritDoc
37+
*/
38+
protected function setUp(): void
39+
{
40+
$objectManager = Bootstrap::getObjectManager();
41+
$this->config = $objectManager->get(ReinitableConfigInterface::class);
42+
$this->factory = $objectManager->get(WysiwygRestrictCommandFactory::class);
43+
}
44+
45+
/**
46+
* "Execute" method cases.
47+
*
48+
* @return array
49+
*/
50+
public function getExecuteCases(): array
51+
{
52+
return [
53+
'yes' => ['y', true],
54+
'no' => ['n', false],
55+
'no-but-different' => ['what', false]
56+
];
57+
}
58+
59+
/**
60+
* Test the command.
61+
*
62+
* @param string $argument
63+
* @param bool $expectedFlag
64+
* @return void
65+
* @dataProvider getExecuteCases
66+
* @magentoConfigFixture default_store cms/wysiwyg/force_valid 0
67+
*/
68+
public function testExecute(string $argument, bool $expectedFlag): void
69+
{
70+
/** @var WysiwygRestrictCommand $model */
71+
$model = $this->factory->create();
72+
$tester = new CommandTester($model);
73+
$tester->execute(['restrict' => $argument]);
74+
75+
$this->config->reinit();
76+
$this->assertEquals($expectedFlag, $this->config->isSetFlag(Validator::CONFIG_PATH_THROW_EXCEPTION));
77+
}
78+
}

0 commit comments

Comments
 (0)