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
Lines changed: 40 additions & 0 deletions
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+
}
Lines changed: 52 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 0 additions & 4 deletions
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

Lines changed: 10 additions & 5 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 0 additions & 1 deletion
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">

0 commit comments

Comments
 (0)