Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 213c03a

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

File tree

17 files changed

+159
-43
lines changed

17 files changed

+159
-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 <[email protected]>
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 <[email protected]>
20+
*/
21+
final class HtmlSanitizerExtension extends AbstractExtension
22+
{
23+
public function __construct(
24+
private ContainerInterface $sanitizers,
25+
private string $defaultName = '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->defaultName)->sanitize($html);
39+
}
40+
}

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: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,9 @@ public function load(array $configs, ContainerBuilder $container)
485485
$container->removeDefinition('form.type_extension.form.validator');
486486
$container->removeDefinition('form.type_guesser.validator');
487487
}
488+
if (!$this->isConfigEnabled($container, $config['html_sanitizer'])) {
489+
$container->removeDefinition('form.type_extension.form.html_sanitizer');
490+
}
488491
} else {
489492
$container->removeDefinition('console.command.form_debug');
490493
}
@@ -2740,13 +2743,14 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil
27402743

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

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

27522756
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,10 +19,12 @@
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;
2425
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
2526
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
27+
use Symfony\Component\Form\Extension\HttpFoundation\Type\TextTypeHtmlSanitizerExtension;
2628
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
2729
use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension;
2830
use Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension;
@@ -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">

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

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

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

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

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

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

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

Lines changed: 4 additions & 0 deletions
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)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Form\Extension\HttpFoundation\Type;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\Form\AbstractTypeExtension;
16+
use Symfony\Component\Form\Extension\Core\Type\TextType;
17+
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\Form\FormEvent;
19+
use Symfony\Component\Form\FormEvents;
20+
use Symfony\Component\OptionsResolver\OptionsResolver;
21+
22+
/**
23+
* @author Titouan Galopin <[email protected]>
24+
*/
25+
class TextTypeHtmlSanitizerExtension extends AbstractTypeExtension
26+
{
27+
public function __construct(
28+
private ContainerInterface $sanitizers,
29+
private string $defaultName = 'default',
30+
) {
31+
}
32+
33+
public static function getExtendedTypes(): iterable
34+
{
35+
return [TextType::class];
36+
}
37+
38+
public function configureOptions(OptionsResolver $resolver)
39+
{
40+
$resolver
41+
->setDefaults(['sanitize_html' => false, 'sanitizer' => null])
42+
->setAllowedTypes('sanitize_html', 'bool')
43+
->setAllowedTypes('sanitizer', ['string', 'null'])
44+
;
45+
}
46+
47+
public function buildForm(FormBuilderInterface $builder, array $options)
48+
{
49+
if (!$options['sanitize_html']) {
50+
return;
51+
}
52+
53+
$sanitizers = $this->sanitizers;
54+
$sanitizer = $options['sanitizer'] ?? $this->defaultName;
55+
56+
$builder->addEventListener(
57+
FormEvents::PRE_SUBMIT,
58+
static function (FormEvent $event) use ($sanitizers, $sanitizer) {
59+
if (is_scalar($data = $event->getData()) && '' !== trim($data)) {
60+
$event->setData($sanitizers->get($sanitizer)->sanitize($data));
61+
}
62+
},
63+
10000 /* as soon as possible */
64+
);
65+
}
66+
}

0 commit comments

Comments
 (0)