Skip to content

Commit

Permalink
Allow disallowed raw HTML tags to be configurable (#507)
Browse files Browse the repository at this point in the history
  • Loading branch information
colinodell committed Jun 27, 2020
1 parent 6fb7958 commit 212a668
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .phpstorm.meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
expectedArguments(\League\CommonMark\Node\Inline\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types'));
expectedReturnValues(\League\CommonMark\Node\Inline\Newline::getType(), argumentsSet('league_commonmark_newline_types'));

registerArgumentsSet('league_commonmark_options', 'renderer', 'renderer/block_separator', 'renderer/inner_separator', 'renderer/soft_break', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'external_link', 'external_link/nofollow', 'external_link/noopener', 'external_link/noreferrer', 'footnote', 'footnote/backref_class', 'footnote/container_add_hr', 'footnote/container_class', 'footnote/ref_class', 'footnote/ref_id_prefix', 'footnote/footnote_class', 'footnote/footnote_id_prefix', 'heading_permalink', 'heading_permalink/html_class', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/slug_normalizer', 'heading_permalink/symbol', 'heading_permalink/title', 'table_of_contents', 'table_of_contents/style', 'table_of_contents/normalize', 'table_of_contents/position', 'table_of_contents/html_class', 'table_of_contents/min_heading_level', 'table_of_contents/max_heading_level', 'table_of_contents/placeholder');
registerArgumentsSet('league_commonmark_options', 'renderer', 'renderer/block_separator', 'renderer/inner_separator', 'renderer/soft_break', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'disallowed_raw_html', 'disallowed_raw_html/disallowed_tags', 'external_link', 'external_link/nofollow', 'external_link/noopener', 'external_link/noreferrer', 'footnote', 'footnote/backref_class', 'footnote/container_add_hr', 'footnote/container_class', 'footnote/ref_class', 'footnote/ref_id_prefix', 'footnote/footnote_class', 'footnote/footnote_id_prefix', 'heading_permalink', 'heading_permalink/html_class', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/slug_normalizer', 'heading_permalink/symbol', 'heading_permalink/title', 'table_of_contents', 'table_of_contents/style', 'table_of_contents/normalize', 'table_of_contents/position', 'table_of_contents/html_class', 'table_of_contents/min_heading_level', 'table_of_contents/max_heading_level', 'table_of_contents/placeholder');
expectedArguments(\League\CommonMark\Environment\EnvironmentInterface::getConfig(), 0, argumentsSet('league_commonmark_options'));
expectedArguments(\League\CommonMark\Configuration\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options'));
expectedArguments(\League\CommonMark\Configuration\ConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options'));
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ See <https://commonmark.thephpleague.com/2.0/upgrading/> for detailed informatio

### Added

- Added the ability to configure disallowed raw HTML tags (#507)
- Added new `HtmlFilter` and `StringContainerHelper` utility classes
- Added new `AbstractBlockContinueParser` class to simplify the creation of custom block parsers
- Added several new classes and interfaces:
Expand Down
19 changes: 18 additions & 1 deletion docs/2.0/extensions/disallowed-raw-html.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,24 @@ $environment = Environment::createCommonMarkEnvironment();
// Add this extension
$environment->addExtension(new DisallowedRawHTMLExtension());

// Customize the extension's configuration if needed
// Default values are shown below - you can omit this configuration if you're happy with those defaults
// and don't want to customize them
$config = [
'disallowed_raw_html' => [
'disallowed_tags' => ['title', 'textarea', 'style', 'xmp', 'iframe', 'noembed', 'noframes', 'script', 'plaintext'],
],
];

// Instantiate the converter engine and start converting some Markdown!
$converter = new CommonMarkConverter([], $environment);
$converter = new CommonMarkConverter($config, $environment);
echo $converter->convertToHtml('I cannot change the page <title>anymore</title>');
```

## Configuration

This extension can be configured by providing a `disallowed_raw_html` array with the following nested configuration options. The defaults are shown in the code example above.

### `disallowed_tags`

An `array` containing a list of tags that should be escaped.
30 changes: 29 additions & 1 deletion src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,32 @@

final class DisallowedRawHtmlRenderer implements NodeRendererInterface, ConfigurationAwareInterface
{
private const DEFAULT_DISALLOWED_TAGS = [
'title',
'textarea',
'style',
'xmp',
'iframe',
'noembed',
'noframes',
'script',
'plaintext',
];

/**
* @var NodeRendererInterface
*
* @psalm-readonly
*/
private $innerRenderer;

/**
* @var ConfigurationInterface
*
* @psalm-readonly-allow-private-mutation
*/
private $config;

public function __construct(NodeRendererInterface $innerRenderer)
{
$this->innerRenderer = $innerRenderer;
Expand All @@ -44,12 +63,21 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer)
return '';
}

$tags = (array) $this->config->get('disallowed_raw_html/disallowed_tags', self::DEFAULT_DISALLOWED_TAGS);
if (\count($tags) === 0) {
return $rendered;
}

$regex = \sprintf('/<(\/?(?:%s)[ \/>])/i', \implode('|', \array_map('preg_quote', $tags)));

// Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
return \preg_replace('/<(\/?(?:title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext)[ \/>])/i', '&lt;$1', $rendered);
return \preg_replace($regex, '&lt;$1', $rendered);
}

public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;

if ($this->innerRenderer instanceof ConfigurationAwareInterface) {
$this->innerRenderer->setConfiguration($configuration);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\CommonMark\Tests\Unit\Extension\DisallowedRawHtml;

use League\CommonMark\Configuration\Configuration;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlRenderer;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Tests\Unit\Renderer\FakeChildNodeRenderer;
use PHPUnit\Framework\TestCase;

final class DisallowedRawHtmlRendererTest extends TestCase
{
public function testWithEmptyHtml(): void
{
$mockRenderer = $this->createMock(NodeRendererInterface::class);
$mockRenderer->method('render')->willReturn('');

$renderer = new DisallowedRawHtmlRenderer($mockRenderer);

$this->assertSame('', $renderer->render($this->createMock(Node::class), new FakeChildNodeRenderer()));
}

/**
* @dataProvider dataProviderForTestWithDefaultSettings
*/
public function testWithDefaultSettings(string $input, string $expectedOutput): void
{
$mockRenderer = $this->createMock(NodeRendererInterface::class);
$mockRenderer->method('render')->willReturn($input);

$renderer = new DisallowedRawHtmlRenderer($mockRenderer);
$renderer->setConfiguration(new Configuration());

$this->assertSame($expectedOutput, $renderer->render($this->createMock(Node::class), new FakeChildNodeRenderer()));
}

/**
* @return iterable<mixed>
*/
public function dataProviderForTestWithDefaultSettings(): iterable
{
// Different tag variants
yield ['<title>', '&lt;title>'];
yield ['</title>', '&lt;/title>'];
yield ['<title x="sdf">', '&lt;title x="sdf">'];
yield ['<title/>', '&lt;title/>'];
yield ['<title />', '&lt;title />'];

// Other tags escaped by default
yield ['<textarea>', '&lt;textarea>'];
yield ['<style>', '&lt;style>'];
yield ['<xmp>', '&lt;xmp>'];
yield ['<iframe>', '&lt;iframe>'];
yield ['<noembed>', '&lt;noembed>'];
yield ['<noframes>', '&lt;noframes>'];
yield ['<script>', '&lt;script>'];
yield ['<plaintext>', '&lt;plaintext>'];

// Tags not escaped by default
yield ['<strong>', '<strong>'];
}

/**
* @dataProvider dataProviderForTestWithCustomSettings
*/
public function testWithCustomSettings(string $input, string $expectedOutput): void
{
$mockRenderer = $this->createMock(NodeRendererInterface::class);
$mockRenderer->method('render')->willReturn($input);

$renderer = new DisallowedRawHtmlRenderer($mockRenderer);
$renderer->setConfiguration(new Configuration([
'disallowed_raw_html' => [
'disallowed_tags' => [
'strong',
],
],
]));

$this->assertSame($expectedOutput, $renderer->render($this->createMock(Node::class), new FakeChildNodeRenderer()));
}

/**
* @return iterable<mixed>
*/
public function dataProviderForTestWithCustomSettings(): iterable
{
// Tags that I've configured to escape
yield ['<strong>', '&lt;strong>'];
yield ['</strong>', '&lt;/strong>'];
yield ['<strong x="sdf">', '&lt;strong x="sdf">'];
yield ['<strong/>', '&lt;strong/>'];
yield ['<strong />', '&lt;strong />'];

// Defaults that I didn't include in my custom config
yield ['<title>', '<title>'];
yield ['<textarea>', '<textarea>'];
yield ['<style>', '<style>'];
yield ['<xmp>', '<xmp>'];
yield ['<iframe>', '<iframe>'];
yield ['<noembed>', '<noembed>'];
yield ['<noframes>', '<noframes>'];
yield ['<script>', '<script>'];
yield ['<plaintext>', '<plaintext>'];
}
}

0 comments on commit 212a668

Please sign in to comment.