Skip to content

Commit 5ad233a

Browse files
authored
Merge pull request #1125 from phpDocumentor/feature/sanitize-raw-html
[FEAT] add support for raw html sanitizers
2 parents d80a23c + 7378a62 commit 5ad233a

File tree

20 files changed

+601
-112
lines changed

20 files changed

+601
-112
lines changed

composer.lock

Lines changed: 306 additions & 94 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/guides-cli/resources/schema/guides.xsd

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<xsd:element name="ignored_domain" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
1818
<xsd:element name="inventory" type="inventory" minOccurs="0" maxOccurs="unbounded"/>
1919
<xsd:element name="template" type="template" minOccurs="0" maxOccurs="unbounded"/>
20+
<xsd:element name="raw-node" type="raw-node" minOccurs="0" maxOccurs="1" />
2021
</xsd:choice>
2122

2223
<xsd:attribute name="input" type="xsd:string"/>
@@ -33,7 +34,6 @@
3334
<xsd:attribute name="links-are-relative" type="xsd:string"/>
3435
<xsd:attribute name="automatic-menu" type="xsd:string"/>
3536
<xsd:attribute name="max-menu-depth" type="xsd:int"/>
36-
3737
</xsd:complexType>
3838

3939
<xsd:complexType name="extension">
@@ -72,4 +72,24 @@
7272
<xsd:attribute name="node" type="xsd:string" use="required"/>
7373
<xsd:attribute name="format" type="xsd:string" default="html"/>
7474
</xsd:complexType>
75+
76+
<xsd:complexType name="raw-node">
77+
<xsd:choice minOccurs="0" maxOccurs="unbounded">
78+
<xsd:element name="sanitizer" type="sanitizer" minOccurs="0" maxOccurs="unbounded" />
79+
</xsd:choice>
80+
<xsd:attribute name="escape" type="xsd:boolean" />
81+
<xsd:attribute name="sanitizer-name" type="xsd:string" default="default" />
82+
</xsd:complexType>
83+
84+
<xsd:complexType name="sanitizer">
85+
<xsd:choice minOccurs="0" maxOccurs="unbounded">
86+
<xsd:element name="allow-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
87+
<xsd:element name="block-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
88+
<xsd:element name="drop-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
89+
<xsd:element name="allow-attribute" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
90+
<xsd:element name="drop-attribute" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
91+
</xsd:choice>
92+
93+
<xsd:attribute name="name" type="xsd:string" use="required" />
94+
</xsd:complexType>
7595
</xsd:schema>

packages/guides-markdown/src/Markdown/Parsers/HtmlParser.php

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,20 @@
1818
use League\CommonMark\Node\NodeWalker;
1919
use League\CommonMark\Node\NodeWalkerEvent;
2020
use phpDocumentor\Guides\MarkupLanguageParser;
21-
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
22-
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
23-
use phpDocumentor\Guides\Nodes\ParagraphNode;
24-
use Psr\Log\LoggerInterface;
21+
use phpDocumentor\Guides\Nodes\RawNode;
2522

2623
use function assert;
2724

28-
/** @extends AbstractBlockParser<ParagraphNode> */
25+
/** @extends AbstractBlockParser<RawNode> */
2926
final class HtmlParser extends AbstractBlockParser
3027
{
31-
public function __construct(
32-
private readonly LoggerInterface $logger,
33-
) {
34-
}
35-
36-
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): ParagraphNode
28+
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): RawNode
3729
{
3830
assert($current instanceof HtmlBlock);
3931

40-
$this->logger->warning('We do not support plain HTML for security reasons. Escaping all HTML ');
41-
4232
$walker->next();
4333

44-
return new ParagraphNode([new InlineCompoundNode([new PlainTextInlineNode($current->getLiteral())])]);
34+
return new RawNode($current->getLiteral());
4535
}
4636

4737
public function supports(NodeWalkerEvent $event): bool

packages/guides-restructured-text/src/RestructuredText/Directives/RawDirective.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public function process(
4141
BlockContext $blockContext,
4242
Directive $directive,
4343
): Node|null {
44-
return new RawNode(implode("\n", $blockContext->getDocumentIterator()->toArray()));
44+
return new RawNode(
45+
implode("\n", $blockContext->getDocumentIterator()->toArray()),
46+
$directive->getData(),
47+
);
4548
}
4649
}

packages/guides/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
"phpdocumentor/flyfinder": "^1.1",
3232
"psr/event-dispatcher": "^1.0",
3333
"symfony/clock": "^6.4.8",
34+
"symfony/html-sanitizer": "^6.4.8",
35+
"symfony/http-client": "^6.4.9",
3436
"symfony/string": "^6.4.9",
3537
"symfony/translation-contracts": "^3.5.0",
36-
"symfony/http-client": "^6.4.9",
3738
"twig/twig": "~2.15 || ^3.0",
3839
"webmozart/assert": "^1.11"
3940
},

packages/guides/resources/config/guides.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use phpDocumentor\Guides\Compiler\NodeTransformers\CustomNodeTransformerFactory;
1111
use phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers\InternalMenuEntryNodeTransformer;
1212
use phpDocumentor\Guides\Compiler\NodeTransformers\NodeTransformerFactory;
13+
use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer;
1314
use phpDocumentor\Guides\Event\PostProjectNodeCreated;
1415
use phpDocumentor\Guides\EventListener\LoadSettingsFromComposer;
1516
use phpDocumentor\Guides\NodeRenderers\Html\BreadCrumbNodeRenderer;
@@ -62,6 +63,7 @@
6263
use phpDocumentor\Guides\Twig\TwigTemplateRenderer;
6364
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6465
use Symfony\Component\DependencyInjection\Reference;
66+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
6567
use Symfony\Component\HttpClient\HttpClient;
6668
use Symfony\Contracts\HttpClient\HttpClientInterface;
6769
use Twig\Loader\FilesystemLoader;
@@ -105,6 +107,9 @@
105107
->set(InternalMenuEntryNodeTransformer::class)
106108
->tag('phpdoc.guides.compiler.nodeTransformers')
107109

110+
->set(RawNodeEscapeTransformer::class)
111+
->arg('$escapeRawNodes', param('phpdoc.guides.raw_node.escape'))
112+
->arg('$htmlSanitizerConfig', service('phpdoc.guides.raw_node.sanitizer.default'))
108113

109114
->set(AbsoluteUrlGenerator::class)
110115
->set(RelativeUrlGenerator::class)
@@ -244,5 +249,8 @@
244249
->arg('$themeManager', service(ThemeManager::class))
245250

246251
->set(TemplateRenderer::class, TwigTemplateRenderer::class)
247-
->arg('$environmentBuilder', new Reference(EnvironmentBuilder::class));
252+
->arg('$environmentBuilder', new Reference(EnvironmentBuilder::class))
253+
254+
->set('phpdoc.guides.raw_node.sanitizer.default', HtmlSanitizerConfig::class)
255+
->call('allowSafeElements', [], true);
248256
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
15+
16+
use phpDocumentor\Guides\Compiler\CompilerContext;
17+
use phpDocumentor\Guides\Compiler\NodeTransformer;
18+
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
19+
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
20+
use phpDocumentor\Guides\Nodes\Node;
21+
use phpDocumentor\Guides\Nodes\ParagraphNode;
22+
use phpDocumentor\Guides\Nodes\RawNode;
23+
use Psr\Log\LoggerInterface;
24+
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
25+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
26+
27+
use function assert;
28+
29+
/** @implements NodeTransformer<Node> */
30+
final class RawNodeEscapeTransformer implements NodeTransformer
31+
{
32+
private HtmlSanitizer $htmlSanitizer;
33+
34+
public function __construct(
35+
private readonly bool $escapeRawNodes,
36+
private readonly LoggerInterface $logger,
37+
HtmlSanitizerConfig $htmlSanitizerConfig,
38+
) {
39+
$this->htmlSanitizer = new HtmlSanitizer($htmlSanitizerConfig);
40+
}
41+
42+
public function enterNode(Node $node, CompilerContext $compilerContext): Node
43+
{
44+
return $node;
45+
}
46+
47+
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
48+
{
49+
assert($node instanceof RawNode);
50+
if ($this->escapeRawNodes) {
51+
$this->logger->warning('We do not support plain HTML for security reasons. Escaping all HTML ');
52+
53+
return new ParagraphNode([new InlineCompoundNode([new PlainTextInlineNode($node->getValue())])]);
54+
}
55+
56+
if ($node->getOption('format', 'html') === 'html') {
57+
return new RawNode($this->htmlSanitizer->sanitize($node->getValue()));
58+
}
59+
60+
return $node;
61+
}
62+
63+
public function supports(Node $node): bool
64+
{
65+
return $node instanceof RawNode;
66+
}
67+
68+
public function getPriority(): int
69+
{
70+
return 1000;
71+
}
72+
}

packages/guides/src/DependencyInjection/GuidesExtension.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace phpDocumentor\Guides\DependencyInjection;
1515

16+
use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer;
1617
use phpDocumentor\Guides\DependencyInjection\Compiler\NodeRendererPass;
1718
use phpDocumentor\Guides\DependencyInjection\Compiler\ParserRulesPass;
1819
use phpDocumentor\Guides\DependencyInjection\Compiler\RendererPass;
@@ -31,6 +32,8 @@
3132
use Symfony\Component\DependencyInjection\Extension\Extension;
3233
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
3334
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
35+
use Symfony\Component\DependencyInjection\Reference;
36+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
3437

3538
use function array_keys;
3639
use function array_map;
@@ -163,6 +166,33 @@ static function ($value) {
163166
->end()
164167
->end()
165168
->end()
169+
->arrayNode('raw_node')
170+
->fixXmlConfig('sanitizer')
171+
->children()
172+
->booleanNode('escape')->defaultValue(false)->end()
173+
->scalarNode('sanitizer_name')->end()
174+
->arrayNode('sanitizers')
175+
->defaultValue([])
176+
->arrayPrototype()
177+
->fixXmlConfig('allow_element')
178+
->fixXmlConfig('drop_element')
179+
->fixXmlConfig('block_element')
180+
->fixXmlConfig('allow_attribute')
181+
->fixXmlConfig('drop_attribute')
182+
->children()
183+
->scalarNode('name')->isRequired()->end()
184+
->booleanNode('allow_safe_elements')->defaultValue(true)->end()
185+
->booleanNode('allow_static_elements')->defaultValue(true)->end()
186+
->arrayNode('allow_elements')->scalarPrototype()->end()->end()
187+
->arrayNode('block_elements')->scalarPrototype()->end()->end()
188+
->arrayNode('drop_elements')->scalarPrototype()->end()->end()
189+
->arrayNode('allow_attributes')->scalarPrototype()->end()->end()
190+
->arrayNode('drop_attributes')->scalarPrototype()->end()->end()
191+
->end()
192+
->end()
193+
->end()
194+
->end()
195+
->end()
166196
->scalarNode('default_code_language')->defaultValue('')->end()
167197
->arrayNode('themes')
168198
->defaultValue([])
@@ -295,6 +325,11 @@ public function load(array $configs, ContainerBuilder $container): void
295325
$container->setParameter('phpdoc.guides.base_template_paths', $config['base_template_paths']);
296326
$container->setParameter('phpdoc.guides.node_templates', $config['templates']);
297327
$container->setParameter('phpdoc.guides.inventories', $config['inventories']);
328+
$container->setParameter('phpdoc.guides.raw_node.escape', $config['raw_node']['escape'] ?? false);
329+
330+
if ($config['raw_node'] ?? false) {
331+
$this->configureSanitizers($config['raw_node'], $container);
332+
}
298333

299334
foreach ($config['themes'] as $themeName => $themeConfig) {
300335
$container->getDefinition(ThemeManager::class)
@@ -333,6 +368,54 @@ public function prepend(ContainerBuilder $container): void
333368
],
334369
);
335370
}
371+
372+
/** @param array<string, mixed> $rawNodeConfig */
373+
private function configureSanitizers(array $rawNodeConfig, ContainerBuilder $container): void
374+
{
375+
if ($rawNodeConfig['sanitizer_name'] ?? false) {
376+
$container->getDefinition(RawNodeEscapeTransformer::class)
377+
->setArgument('$htmlSanitizerConfig', new Reference('phpdoc.guides.raw_node.sanitizer.' . $rawNodeConfig['sanitizer_name']));
378+
}
379+
380+
if (!is_array($rawNodeConfig['sanitizers'] ?? false)) {
381+
return;
382+
}
383+
384+
foreach ($rawNodeConfig['sanitizers'] as $sanitizerConfig) {
385+
$def = $container->register('phpdoc.guides.raw_node.sanitizer.' . $sanitizerConfig['name'], HtmlSanitizerConfig::class);
386+
387+
// Base
388+
if ($sanitizerConfig['allow_safe_elements']) {
389+
$def->addMethodCall('allowSafeElements', [], true);
390+
}
391+
392+
if ($sanitizerConfig['allow_static_elements']) {
393+
$def->addMethodCall('allowStaticElements', [], true);
394+
}
395+
396+
// Configures elements
397+
foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) {
398+
$def->addMethodCall('allowElement', [$element, $attributes], true);
399+
}
400+
401+
foreach ($sanitizerConfig['block_elements'] as $element) {
402+
$def->addMethodCall('blockElement', [$element], true);
403+
}
404+
405+
foreach ($sanitizerConfig['drop_elements'] as $element) {
406+
$def->addMethodCall('dropElement', [$element], true);
407+
}
408+
409+
// Configures attributes
410+
foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) {
411+
$def->addMethodCall('allowAttribute', [$attribute, $elements], true);
412+
}
413+
414+
foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) {
415+
$def->addMethodCall('dropAttribute', [$attribute, $elements], true);
416+
}
417+
}
418+
}
336419
}
337420

338421
/**

packages/guides/src/Nodes/RawNode.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@
1515

1616
final class RawNode extends TextNode
1717
{
18+
public function __construct(string $contents, string $format = 'html')
19+
{
20+
parent::__construct($contents);
21+
22+
$this->options['format'] = $format;
23+
}
1824
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!-- content start -->
2+
<div class="section" id="directive-tests">
3+
<h1>Directive tests</h1>
4+
<p>Lorem Ipsum Dolor</p>
5+
<p>Dolor sit!</p>
6+
</div>
7+
<!-- content end -->
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<guides xmlns="https://www.phpdoc.org/guides"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="https://www.phpdoc.org/guides packages/guides-cli/resources/schema/guides.xsd"
5+
input-format="rst"
6+
>
7+
<project title="Project Title" version="6.4"/>
8+
<raw-node sanitizer-name="custom" >
9+
<sanitizer name="custom">
10+
<allow-element>p</allow-element>
11+
<block-element>div</block-element>
12+
<drop-element>h2</drop-element>
13+
</sanitizer>
14+
</raw-node>
15+
</guides>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Directive tests
2+
===============
3+
4+
.. raw:: html
5+
6+
<div class="someClass">
7+
<script>alert('XSS');</script>
8+
<p>Lorem Ipsum Dolor</p>
9+
<h2>Some Rubric</h2>
10+
<p>Dolor sit!</p>
11+
</div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- content start -->
2+
<div class="section" id="directive-tests">
3+
<h1>Directive tests</h1>
4+
<div>
5+
<p>Lorem Ipsum Dolor</p>
6+
<h2>Some Rubric</h2>
7+
<p>Dolor sit!</p>
8+
</div>
9+
</div>
10+
<!-- content end -->
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<guides xmlns="https://www.phpdoc.org/guides"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="https://www.phpdoc.org/guides packages/guides-cli/resources/schema/guides.xsd"
5+
input-format="rst"
6+
>
7+
<project title="Project Title" version="6.4"/>
8+
</guides>

0 commit comments

Comments
 (0)