Skip to content

Commit 0ea89e5

Browse files
[FrameworkBundle][TwigBundle][Form] Add Twig filter, form-type extension and improve service definitions for HtmlSanitizer
1 parent 6bed67f commit 0ea89e5

File tree

21 files changed

+312
-43
lines changed

21 files changed

+312
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Extension;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Twig\Extension\AbstractExtension;
16+
use Twig\TwigFilter;
17+
18+
/**
19+
* @author Titouan Galopin <galopintitouan@gmail.com>
20+
*/
21+
final class HtmlSanitizerExtension extends AbstractExtension
22+
{
23+
public function __construct(
24+
private ContainerInterface $sanitizers,
25+
private string $defaultSanitizer = 'default',
26+
) {
27+
}
28+
29+
public function getFilters(): array
30+
{
31+
return [
32+
new TwigFilter('sanitize_html', $this->sanitize(...), ['is_safe' => ['html']]),
33+
];
34+
}
35+
36+
public function sanitize(string $html, string $sanitizer = null): string
37+
{
38+
return $this->sanitizers->get($sanitizer ?? $this->defaultSanitizer)->sanitize($html);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Tests\Extension;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\Twig\Extension\HtmlSanitizerExtension;
16+
use Symfony\Component\DependencyInjection\ServiceLocator;
17+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
18+
use Twig\Environment;
19+
use Twig\Loader\ArrayLoader;
20+
21+
class HtmlSanitizerExtensionTest extends TestCase
22+
{
23+
public function testSanitizeHtml()
24+
{
25+
$loader = new ArrayLoader([
26+
'foo' => '{{ "foobar"|sanitize_html }}',
27+
'bar' => '{{ "foobar"|sanitize_html("bar") }}',
28+
]);
29+
30+
$twig = new Environment($loader, ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]);
31+
32+
$fooSanitizer = $this->createMock(HtmlSanitizerInterface::class);
33+
$fooSanitizer->expects($this->once())
34+
->method('sanitize')
35+
->with('foobar')
36+
->willReturn('foo');
37+
38+
$barSanitizer = $this->createMock(HtmlSanitizerInterface::class);
39+
$barSanitizer->expects($this->once())
40+
->method('sanitize')
41+
->with('foobar')
42+
->willReturn('bar');
43+
44+
$twig->addExtension(new HtmlSanitizerExtension(new ServiceLocator([
45+
'foo' => fn () => $fooSanitizer,
46+
'bar' => fn () => $barSanitizer,
47+
]), 'foo'));
48+
49+
$this->assertSame('foo', $twig->render('foo'));
50+
$this->assertSame('bar', $twig->render('bar'));
51+
}
52+
}

src/Symfony/Bridge/Twig/UndefinedCallableHandler.php

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class UndefinedCallableHandler
2424
private const FILTER_COMPONENTS = [
2525
'humanize' => 'form',
2626
'trans' => 'translation',
27+
'sanitize_html' => 'html-sanitizer',
2728
'yaml_encode' => 'yaml',
2829
'yaml_dump' => 'yaml',
2930
];
@@ -61,6 +62,7 @@ class UndefinedCallableHandler
6162
];
6263

6364
private const FULL_STACK_ENABLE = [
65+
'html-sanitizer' => 'enable "framework.html_sanitizer"',
6466
'form' => 'enable "framework.form"',
6567
'security-core' => 'add the "SecurityBundle"',
6668
'security-http' => 'add the "SecurityBundle"',

src/Symfony/Bridge/Twig/composer.json

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"symfony/dependency-injection": "^5.4|^6.0",
2929
"symfony/finder": "^5.4|^6.0",
3030
"symfony/form": "^6.1",
31+
"symfony/html-sanitizer": "^6.1",
3132
"symfony/http-foundation": "^5.4|^6.0",
3233
"symfony/http-kernel": "^5.4|^6.0",
3334
"symfony/intl": "^5.4|^6.0",
@@ -65,6 +66,7 @@
6566
"symfony/finder": "",
6667
"symfony/asset": "For using the AssetExtension",
6768
"symfony/form": "For using the FormExtension",
69+
"symfony/html-sanitizer": "For using the HtmlSanitizerExtension",
6870
"symfony/http-kernel": "For using the HttpKernelExtension",
6971
"symfony/routing": "For using the RoutingExtension",
7072
"symfony/translation": "For using the TranslationExtension",

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class UnusedTagsPass implements CompilerPassInterface
4949
'form.type',
5050
'form.type_extension',
5151
'form.type_guesser',
52+
'html_sanitizer',
5253
'http_client.client',
5354
'kernel.cache_clearer',
5455
'kernel.cache_warmer',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

-4
Original file line numberDiff line numberDiff line change
@@ -2129,10 +2129,6 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable
21292129
->{$enableIfStandalone('symfony/html-sanitizer', HtmlSanitizerInterface::class)}()
21302130
->fixXmlConfig('sanitizer')
21312131
->children()
2132-
->scalarNode('default')
2133-
->defaultNull()
2134-
->info('Default sanitizer to use when injecting without named binding.')
2135-
->end()
21362132
->arrayNode('sanitizers')
21372133
->useAttributeAsKey('name')
21382134
->arrayPrototype()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
7070
use Symfony\Component\Finder\Finder;
7171
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
72+
use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension;
7273
use Symfony\Component\Form\Form;
7374
use Symfony\Component\Form\FormTypeExtensionInterface;
7475
use Symfony\Component\Form\FormTypeGuesserInterface;
@@ -485,6 +486,9 @@ public function load(array $configs, ContainerBuilder $container)
485486
$container->removeDefinition('form.type_extension.form.validator');
486487
$container->removeDefinition('form.type_guesser.validator');
487488
}
489+
if (!$this->isConfigEnabled($container, $config['html_sanitizer']) || !class_exists(TextTypeHtmlSanitizerExtension::class)) {
490+
$container->removeDefinition('form.type_extension.form.html_sanitizer');
491+
}
488492
} else {
489493
$container->removeDefinition('console.command.form_debug');
490494
}
@@ -2740,13 +2744,14 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil
27402744

27412745
// Create the sanitizer and link its config
27422746
$sanitizerId = 'html_sanitizer.sanitizer.'.$sanitizerName;
2743-
$container->register($sanitizerId, HtmlSanitizer::class)->addArgument(new Reference($configId));
2747+
$container->register($sanitizerId, HtmlSanitizer::class)
2748+
->addTag('html_sanitizer', ['sanitizer' => $sanitizerName])
2749+
->addArgument(new Reference($configId));
27442750

2745-
$container->registerAliasForArgument($sanitizerId, HtmlSanitizerInterface::class, $sanitizerName);
2751+
if ('default' !== $sanitizerName) {
2752+
$container->registerAliasForArgument($sanitizerId, HtmlSanitizerInterface::class, $sanitizerName);
2753+
}
27462754
}
2747-
2748-
$default = $config['default'] ? 'html_sanitizer.sanitizer.'.$config['default'] : 'html_sanitizer';
2749-
$container->setAlias(HtmlSanitizerInterface::class, new Reference($default));
27502755
}
27512756

27522757
private function resolveTrustedHeaders(array $headers): int

src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php

+6
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
use Symfony\Component\Form\Extension\Core\Type\FileType;
2020
use Symfony\Component\Form\Extension\Core\Type\FormType;
2121
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
22+
use Symfony\Component\Form\Extension\Core\Type\TextType;
2223
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
2324
use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension;
25+
use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension;
2426
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
2527
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
2628
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
@@ -113,6 +115,10 @@
113115
->args([service('translator')->ignoreOnInvalid()])
114116
->tag('form.type_extension', ['extended-type' => FormType::class])
115117

118+
->set('form.type_extension.form.html_sanitizer', TextTypeHtmlSanitizerExtension::class)
119+
->args([tagged_locator('html_sanitizer', 'sanitizer')])
120+
->tag('form.type_extension', ['extended-type' => TextType::class])
121+
116122
->set('form.type_extension.form.http_foundation', FormTypeHttpFoundationExtension::class)
117123
->args([service('form.type_extension.form.request_handler')])
118124
->tag('form.type_extension')

src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php

+7-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@
1313

1414
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
1515
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
16+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
1617

1718
return static function (ContainerConfigurator $container) {
1819
$container->services()
19-
->set('html_sanitizer.config', HtmlSanitizerConfig::class)
20+
->set('html_sanitizer.config.default', HtmlSanitizerConfig::class)
2021
->call('allowSafeElements')
2122

22-
->set('html_sanitizer', HtmlSanitizer::class)
23+
->set('html_sanitizer.sanitizer.default', HtmlSanitizer::class)
2324
->args([service('html_sanitizer.config')])
25+
->tag('html_sanitizer', ['name' => 'default'])
26+
27+
->alias('html_sanitizer', 'html_sanitizer.sanitizer.default')
28+
->alias(HtmlSanitizerInterface::class, 'html_sanitizer')
2429
;
2530
};

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

-1
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,6 @@
826826
<xsd:element name="sanitizer" type="sanitizer" minOccurs="0" maxOccurs="unbounded" />
827827
</xsd:sequence>
828828
<xsd:attribute name="enabled" type="xsd:boolean" />
829-
<xsd:attribute name="default" type="xsd:string" />
830829
</xsd:complexType>
831830

832831
<xsd:complexType name="sanitizer">

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

-1
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,6 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
652652
],
653653
'html_sanitizer' => [
654654
'enabled' => !class_exists(FullStack::class) && class_exists(HtmlSanitizer::class),
655-
'default' => null,
656655
'sanitizers' => [],
657656
],
658657
'exceptions' => [],

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
$container->loadFromExtension('framework', [
44
'http_method_override' => false,
55
'html_sanitizer' => [
6-
'default' => 'my.sanitizer',
76
'sanitizers' => [
8-
'my.sanitizer' => [
7+
'default' => [
98
'allow_safe_elements' => true,
109
'allow_all_static_elements' => true,
1110
'allow_elements' => [

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
77

88
<config xmlns="http://symfony.com/schema/dic/symfony" http-method-override="false">
9-
<html-sanitizer default="my.sanitizer">
10-
<sanitizer name="my.sanitizer"
9+
<html-sanitizer>
10+
<sanitizer name="default"
1111
allow-safe-elements="true"
1212
allow-all-static-elements="true"
1313
force-https-urls="true"

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
framework:
22
http_method_override: false
33
html_sanitizer:
4-
default: my.sanitizer
54
sanitizers:
6-
my.sanitizer:
5+
default:
76
allow_safe_elements: true
87
allow_all_static_elements: true
98
allow_elements:

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

+13-24
Original file line numberDiff line numberDiff line change
@@ -2030,27 +2030,16 @@ public function testHtmlSanitizer()
20302030
$container = $this->createContainerFromFile('html_sanitizer');
20312031

20322032
// html_sanitizer service
2033-
$this->assertTrue($container->hasDefinition('html_sanitizer'), '->registerHtmlSanitizerConfiguration() loads html_sanitizer.php');
2034-
$this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer')->getClass());
2035-
$this->assertCount(1, $args = $container->getDefinition('html_sanitizer')->getArguments());
2036-
$this->assertSame('html_sanitizer.config', (string) $args[0]);
2037-
2038-
// html_sanitizer.config service
2039-
$this->assertTrue($container->hasDefinition('html_sanitizer.config'), '->registerHtmlSanitizerConfiguration() loads html_sanitizer.php');
2040-
$this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config')->getClass());
2041-
$this->assertCount(1, $calls = $container->getDefinition('html_sanitizer.config')->getMethodCalls());
2042-
$this->assertSame(['allowSafeElements', []], $calls[0]);
2043-
2044-
// my.sanitizer
2045-
$this->assertTrue($container->hasDefinition('html_sanitizer.sanitizer.my.sanitizer'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer');
2046-
$this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer.sanitizer.my.sanitizer')->getClass());
2047-
$this->assertCount(1, $args = $container->getDefinition('html_sanitizer.sanitizer.my.sanitizer')->getArguments());
2048-
$this->assertSame('html_sanitizer.config.my.sanitizer', (string) $args[0]);
2049-
2050-
// my.sanitizer config
2051-
$this->assertTrue($container->hasDefinition('html_sanitizer.config.my.sanitizer'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer');
2052-
$this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.my.sanitizer')->getClass());
2053-
$this->assertCount(23, $calls = $container->getDefinition('html_sanitizer.config.my.sanitizer')->getMethodCalls());
2033+
$this->assertTrue($container->hasAlias('html_sanitizer'), '->registerHtmlSanitizerConfiguration() loads html_sanitizer.php');
2034+
$this->assertSame('html_sanitizer.sanitizer.default', (string) $container->getAlias('html_sanitizer'));
2035+
$this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer.sanitizer.default')->getClass());
2036+
$this->assertCount(1, $args = $container->getDefinition('html_sanitizer.sanitizer.default')->getArguments());
2037+
$this->assertSame('html_sanitizer.config.default', (string) $args[0]);
2038+
2039+
// config
2040+
$this->assertTrue($container->hasDefinition('html_sanitizer.config.default'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer');
2041+
$this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.default')->getClass());
2042+
$this->assertCount(23, $calls = $container->getDefinition('html_sanitizer.config.default')->getMethodCalls());
20542043
$this->assertSame(
20552044
[
20562045
['allowSafeElements', [], true],
@@ -2092,11 +2081,11 @@ static function ($call) {
20922081
);
20932082

20942083
// Named alias
2095-
$this->assertSame('html_sanitizer.sanitizer.my.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $mySanitizer'), '->registerHtmlSanitizerConfiguration() creates appropriate named alias');
2096-
$this->assertSame('html_sanitizer.sanitizer.all.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $allSanitizer'), '->registerHtmlSanitizerConfiguration() creates appropriate named alias');
2084+
$this->assertSame('html_sanitizer.sanitizer.all.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $allSanitizer'));
2085+
$this->assertFalse($container->hasAlias(HtmlSanitizerInterface::class.' $default'));
20972086

20982087
// Default alias
2099-
$this->assertSame('html_sanitizer.sanitizer.my.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class), '->registerHtmlSanitizerConfiguration() creates appropriate default alias');
2088+
$this->assertSame('html_sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class));
21002089
}
21012090

21022091
protected function createContainer(array $data = [])

src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\DependencyInjection\Reference;
2020
use Symfony\Component\Form\AbstractRendererEngine;
2121
use Symfony\Component\Form\Form;
22+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
2223
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
2324
use Symfony\Component\Mailer\Mailer;
2425
use Symfony\Component\Translation\Translator;
@@ -54,6 +55,10 @@ public function load(array $configs, ContainerBuilder $container)
5455
$loader->load('console.php');
5556
}
5657

58+
if (!$container::willBeAvailable('symfony/html-sanitizer', HtmlSanitizerInterface::class, ['symfony/twig-bundle'])) {
59+
$container->removeDefinition('twig.extension.htmlsanitizer');
60+
}
61+
5762
if ($container::willBeAvailable('symfony/mailer', Mailer::class, ['symfony/twig-bundle'])) {
5863
$loader->load('mailer.php');
5964
}

src/Symfony/Bundle/TwigBundle/Resources/config/twig.php

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Bridge\Twig\Extension\AssetExtension;
1919
use Symfony\Bridge\Twig\Extension\CodeExtension;
2020
use Symfony\Bridge\Twig\Extension\ExpressionExtension;
21+
use Symfony\Bridge\Twig\Extension\HtmlSanitizerExtension;
2122
use Symfony\Bridge\Twig\Extension\HttpFoundationExtension;
2223
use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
2324
use Symfony\Bridge\Twig\Extension\HttpKernelRuntime;
@@ -118,6 +119,9 @@
118119

119120
->set('twig.extension.expression', ExpressionExtension::class)
120121

122+
->set('twig.extension.htmlsanitizer', HtmlSanitizerExtension::class)
123+
->args([tagged_locator('html_sanitizer', 'sanitizer')])
124+
121125
->set('twig.extension.httpkernel', HttpKernelExtension::class)
122126

123127
->set('twig.runtime.httpkernel', HttpKernelRuntime::class)

0 commit comments

Comments
 (0)