diff --git a/src/Symfony/Bridge/Twig/Extension/HtmlSanitizerExtension.php b/src/Symfony/Bridge/Twig/Extension/HtmlSanitizerExtension.php new file mode 100644 index 0000000000000..bec5ceb94e34e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/HtmlSanitizerExtension.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Psr\Container\ContainerInterface; +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; + +/** + * @author Titouan Galopin + */ +final class HtmlSanitizerExtension extends AbstractExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + public function getFilters(): array + { + return [ + new TwigFilter('sanitize_html', $this->sanitize(...), ['is_safe' => ['html']]), + ]; + } + + public function sanitize(string $html, string $sanitizer = null): string + { + return $this->sanitizers->get($sanitizer ?? $this->defaultSanitizer)->sanitize($html); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HtmlSanitizerExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HtmlSanitizerExtensionTest.php new file mode 100644 index 0000000000000..23755d0317be2 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HtmlSanitizerExtensionTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Extension\HtmlSanitizerExtension; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; +use Twig\Environment; +use Twig\Loader\ArrayLoader; + +class HtmlSanitizerExtensionTest extends TestCase +{ + public function testSanitizeHtml() + { + $loader = new ArrayLoader([ + 'foo' => '{{ "foobar"|sanitize_html }}', + 'bar' => '{{ "foobar"|sanitize_html("bar") }}', + ]); + + $twig = new Environment($loader, ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]); + + $fooSanitizer = $this->createMock(HtmlSanitizerInterface::class); + $fooSanitizer->expects($this->once()) + ->method('sanitize') + ->with('foobar') + ->willReturn('foo'); + + $barSanitizer = $this->createMock(HtmlSanitizerInterface::class); + $barSanitizer->expects($this->once()) + ->method('sanitize') + ->with('foobar') + ->willReturn('bar'); + + $twig->addExtension(new HtmlSanitizerExtension(new ServiceLocator([ + 'foo' => fn () => $fooSanitizer, + 'bar' => fn () => $barSanitizer, + ]), 'foo')); + + $this->assertSame('foo', $twig->render('foo')); + $this->assertSame('bar', $twig->render('bar')); + } +} diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php index 30dd92ff2ff3b..472330a859c6e 100644 --- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php +++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php @@ -24,6 +24,7 @@ class UndefinedCallableHandler private const FILTER_COMPONENTS = [ 'humanize' => 'form', 'trans' => 'translation', + 'sanitize_html' => 'html-sanitizer', 'yaml_encode' => 'yaml', 'yaml_dump' => 'yaml', ]; @@ -61,6 +62,7 @@ class UndefinedCallableHandler ]; private const FULL_STACK_ENABLE = [ + 'html-sanitizer' => 'enable "framework.html_sanitizer"', 'form' => 'enable "framework.form"', 'security-core' => 'add the "SecurityBundle"', 'security-http' => 'add the "SecurityBundle"', diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 46cea3b115b4d..f182dfa406417 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -28,6 +28,7 @@ "symfony/dependency-injection": "^5.4|^6.0", "symfony/finder": "^5.4|^6.0", "symfony/form": "^6.1", + "symfony/html-sanitizer": "^6.1", "symfony/http-foundation": "^5.4|^6.0", "symfony/http-kernel": "^5.4|^6.0", "symfony/intl": "^5.4|^6.0", @@ -65,6 +66,7 @@ "symfony/finder": "", "symfony/asset": "For using the AssetExtension", "symfony/form": "For using the FormExtension", + "symfony/html-sanitizer": "For using the HtmlSanitizerExtension", "symfony/http-kernel": "For using the HttpKernelExtension", "symfony/routing": "For using the RoutingExtension", "symfony/translation": "For using the TranslationExtension", diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 558111e6a5d43..c1d18f91f7dff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -49,6 +49,7 @@ class UnusedTagsPass implements CompilerPassInterface 'form.type', 'form.type_extension', 'form.type_guesser', + 'html_sanitizer', 'http_client.client', 'kernel.cache_clearer', 'kernel.cache_warmer', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 92f7cbc9a93a0..947f182eef1f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -2129,10 +2129,6 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->{$enableIfStandalone('symfony/html-sanitizer', HtmlSanitizerInterface::class)}() ->fixXmlConfig('sanitizer') ->children() - ->scalarNode('default') - ->defaultNull() - ->info('Default sanitizer to use when injecting without named binding.') - ->end() ->arrayNode('sanitizers') ->useAttributeAsKey('name') ->arrayPrototype() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d21de7da9d1f9..07f15e9b28b52 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -69,6 +69,7 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Finder\Finder; use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormTypeExtensionInterface; use Symfony\Component\Form\FormTypeGuesserInterface; @@ -485,6 +486,9 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('form.type_extension.form.validator'); $container->removeDefinition('form.type_guesser.validator'); } + if (!$this->isConfigEnabled($container, $config['html_sanitizer']) || !class_exists(TextTypeHtmlSanitizerExtension::class)) { + $container->removeDefinition('form.type_extension.form.html_sanitizer'); + } } else { $container->removeDefinition('console.command.form_debug'); } @@ -2740,13 +2744,14 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil // Create the sanitizer and link its config $sanitizerId = 'html_sanitizer.sanitizer.'.$sanitizerName; - $container->register($sanitizerId, HtmlSanitizer::class)->addArgument(new Reference($configId)); + $container->register($sanitizerId, HtmlSanitizer::class) + ->addTag('html_sanitizer', ['sanitizer' => $sanitizerName]) + ->addArgument(new Reference($configId)); - $container->registerAliasForArgument($sanitizerId, HtmlSanitizerInterface::class, $sanitizerName); + if ('default' !== $sanitizerName) { + $container->registerAliasForArgument($sanitizerId, HtmlSanitizerInterface::class, $sanitizerName); + } } - - $default = $config['default'] ? 'html_sanitizer.sanitizer.'.$config['default'] : 'html_sanitizer'; - $container->setAlias(HtmlSanitizerInterface::class, new Reference($default)); } private function resolveTrustedHeaders(array $headers): int diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php index 75bfb7eb651af..3c936a284b325 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php @@ -19,8 +19,10 @@ use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension; use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension; +use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension; use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension; @@ -113,6 +115,10 @@ ->args([service('translator')->ignoreOnInvalid()]) ->tag('form.type_extension', ['extended-type' => FormType::class]) + ->set('form.type_extension.form.html_sanitizer', TextTypeHtmlSanitizerExtension::class) + ->args([tagged_locator('html_sanitizer', 'sanitizer')]) + ->tag('form.type_extension', ['extended-type' => TextType::class]) + ->set('form.type_extension.form.http_foundation', FormTypeHttpFoundationExtension::class) ->args([service('form.type_extension.form.request_handler')]) ->tag('form.type_extension') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php index 558188d18915f..ba87eda1a3357 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php @@ -13,13 +13,18 @@ use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; return static function (ContainerConfigurator $container) { $container->services() - ->set('html_sanitizer.config', HtmlSanitizerConfig::class) + ->set('html_sanitizer.config.default', HtmlSanitizerConfig::class) ->call('allowSafeElements') - ->set('html_sanitizer', HtmlSanitizer::class) + ->set('html_sanitizer.sanitizer.default', HtmlSanitizer::class) ->args([service('html_sanitizer.config')]) + ->tag('html_sanitizer', ['name' => 'default']) + + ->alias('html_sanitizer', 'html_sanitizer.sanitizer.default') + ->alias(HtmlSanitizerInterface::class, 'html_sanitizer') ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index c2bf87f9daf45..2baa0a33a3ebc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -826,7 +826,6 @@ - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index cc567b14829eb..e5b1a947e3313 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -652,7 +652,6 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor ], 'html_sanitizer' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(HtmlSanitizer::class), - 'default' => null, 'sanitizers' => [], ], 'exceptions' => [], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php index 687f05a3ffa2b..de6a0a1ae34bc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php @@ -3,9 +3,8 @@ $container->loadFromExtension('framework', [ 'http_method_override' => false, 'html_sanitizer' => [ - 'default' => 'my.sanitizer', 'sanitizers' => [ - 'my.sanitizer' => [ + 'default' => [ 'allow_safe_elements' => true, 'allow_all_static_elements' => true, 'allow_elements' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml index 77a47724541bf..f2b11618d18e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml @@ -6,8 +6,8 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - + createContainerFromFile('html_sanitizer'); // html_sanitizer service - $this->assertTrue($container->hasDefinition('html_sanitizer'), '->registerHtmlSanitizerConfiguration() loads html_sanitizer.php'); - $this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer')->getClass()); - $this->assertCount(1, $args = $container->getDefinition('html_sanitizer')->getArguments()); - $this->assertSame('html_sanitizer.config', (string) $args[0]); - - // html_sanitizer.config service - $this->assertTrue($container->hasDefinition('html_sanitizer.config'), '->registerHtmlSanitizerConfiguration() loads html_sanitizer.php'); - $this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config')->getClass()); - $this->assertCount(1, $calls = $container->getDefinition('html_sanitizer.config')->getMethodCalls()); - $this->assertSame(['allowSafeElements', []], $calls[0]); - - // my.sanitizer - $this->assertTrue($container->hasDefinition('html_sanitizer.sanitizer.my.sanitizer'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer'); - $this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer.sanitizer.my.sanitizer')->getClass()); - $this->assertCount(1, $args = $container->getDefinition('html_sanitizer.sanitizer.my.sanitizer')->getArguments()); - $this->assertSame('html_sanitizer.config.my.sanitizer', (string) $args[0]); - - // my.sanitizer config - $this->assertTrue($container->hasDefinition('html_sanitizer.config.my.sanitizer'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer'); - $this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.my.sanitizer')->getClass()); - $this->assertCount(23, $calls = $container->getDefinition('html_sanitizer.config.my.sanitizer')->getMethodCalls()); + $this->assertTrue($container->hasAlias('html_sanitizer'), '->registerHtmlSanitizerConfiguration() loads html_sanitizer.php'); + $this->assertSame('html_sanitizer.sanitizer.default', (string) $container->getAlias('html_sanitizer')); + $this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer.sanitizer.default')->getClass()); + $this->assertCount(1, $args = $container->getDefinition('html_sanitizer.sanitizer.default')->getArguments()); + $this->assertSame('html_sanitizer.config.default', (string) $args[0]); + + // config + $this->assertTrue($container->hasDefinition('html_sanitizer.config.default'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer'); + $this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.default')->getClass()); + $this->assertCount(23, $calls = $container->getDefinition('html_sanitizer.config.default')->getMethodCalls()); $this->assertSame( [ ['allowSafeElements', [], true], @@ -2092,11 +2081,11 @@ static function ($call) { ); // Named alias - $this->assertSame('html_sanitizer.sanitizer.my.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $mySanitizer'), '->registerHtmlSanitizerConfiguration() creates appropriate named alias'); - $this->assertSame('html_sanitizer.sanitizer.all.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $allSanitizer'), '->registerHtmlSanitizerConfiguration() creates appropriate named alias'); + $this->assertSame('html_sanitizer.sanitizer.all.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $allSanitizer')); + $this->assertFalse($container->hasAlias(HtmlSanitizerInterface::class.' $default')); // Default alias - $this->assertSame('html_sanitizer.sanitizer.my.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class), '->registerHtmlSanitizerConfiguration() creates appropriate default alias'); + $this->assertSame('html_sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class)); } protected function createContainer(array $data = []) diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index f598524da4d64..adfab4f96610c 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Form\AbstractRendererEngine; use Symfony\Component\Form\Form; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Translation\Translator; @@ -54,6 +55,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('console.php'); } + if (!$container::willBeAvailable('symfony/html-sanitizer', HtmlSanitizerInterface::class, ['symfony/twig-bundle'])) { + $container->removeDefinition('twig.extension.htmlsanitizer'); + } + if ($container::willBeAvailable('symfony/mailer', Mailer::class, ['symfony/twig-bundle'])) { $loader->load('mailer.php'); } diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php index 6a5e52df6b472..5536315306b72 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php @@ -18,6 +18,7 @@ use Symfony\Bridge\Twig\Extension\AssetExtension; use Symfony\Bridge\Twig\Extension\CodeExtension; use Symfony\Bridge\Twig\Extension\ExpressionExtension; +use Symfony\Bridge\Twig\Extension\HtmlSanitizerExtension; use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; use Symfony\Bridge\Twig\Extension\HttpKernelExtension; use Symfony\Bridge\Twig\Extension\HttpKernelRuntime; @@ -118,6 +119,9 @@ ->set('twig.extension.expression', ExpressionExtension::class) + ->set('twig.extension.htmlsanitizer', HtmlSanitizerExtension::class) + ->args([tagged_locator('html_sanitizer', 'sanitizer')]) + ->set('twig.extension.httpkernel', HttpKernelExtension::class) ->set('twig.runtime.httpkernel', HttpKernelRuntime::class) diff --git a/src/Symfony/Component/Form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php b/src/Symfony/Component/Form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php new file mode 100644 index 0000000000000..6c4bf49d6ab8b --- /dev/null +++ b/src/Symfony/Component/Form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HtmlSanitizer; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\AbstractExtension; + +/** + * Integrates the HtmlSanitizer component with the Form library. + * + * @author Nicolas Grekas + */ +class HtmlSanitizerExtension extends AbstractExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\TextTypeHtmlSanitizerExtension($this->sanitizers, $this->defaultSanitizer), + ]; + } +} diff --git a/src/Symfony/Component/Form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php b/src/Symfony/Component/Form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php new file mode 100644 index 0000000000000..0d28c65bce1ad --- /dev/null +++ b/src/Symfony/Component/Form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HtmlSanitizer\Type; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Titouan Galopin + */ +class TextTypeHtmlSanitizerExtension extends AbstractTypeExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + public static function getExtendedTypes(): iterable + { + return [TextType::class]; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults(['sanitize_html' => false, 'sanitizer' => null]) + ->setAllowedTypes('sanitize_html', 'bool') + ->setAllowedTypes('sanitizer', ['string', 'null']) + ; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['sanitize_html']) { + return; + } + + $sanitizers = $this->sanitizers; + $sanitizer = $options['sanitizer'] ?? $this->defaultSanitizer; + + $builder->addEventListener( + FormEvents::PRE_SUBMIT, + static function (FormEvent $event) use ($sanitizers, $sanitizer) { + if (is_scalar($data = $event->getData()) && '' !== trim($data)) { + $event->setData($sanitizers->get($sanitizer)->sanitize($data)); + } + }, + 10000 /* as soon as possible */ + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php new file mode 100644 index 0000000000000..39b8d03323342 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Csrf\Type; + +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\HtmlSanitizer\HtmlSanitizerExtension; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; + +class TextTypeHtmlSanitizerExtensionTest extends TypeTestCase +{ + protected function getExtensions() + { + $fooSanitizer = $this->createMock(HtmlSanitizerInterface::class); + $fooSanitizer->expects($this->once()) + ->method('sanitize') + ->with('foobar') + ->willReturn('foo'); + + $barSanitizer = $this->createMock(HtmlSanitizerInterface::class); + $barSanitizer->expects($this->once()) + ->method('sanitize') + ->with('foobar') + ->willReturn('bar'); + + return array_merge(parent::getExtensions(), [ + new HtmlSanitizerExtension(new ServiceLocator([ + 'foo' => fn () => $fooSanitizer, + 'bar' => fn () => $barSanitizer, + ]), 'foo'), + ]); + } + + public function testSanitizer() + { + $form = $this->factory->createBuilder(FormType::class, ['data' => null]) + ->add('data', TextType::class, ['sanitize_html' => true]) + ->getForm() + ; + $form->submit(['data' => 'foobar']); + + $this->assertSame(['data' => 'foo'], $form->getData()); + + $form = $this->factory->createBuilder(FormType::class, ['data' => null]) + ->add('data', TextType::class, ['sanitize_html' => true, 'sanitizer' => 'bar']) + ->getForm() + ; + $form->submit(['data' => 'foobar']); + + $this->assertSame(['data' => 'bar'], $form->getData()); + } +} diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 6fb2b50866543..05193a2f0c2e8 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -33,6 +33,7 @@ "symfony/expression-language": "^5.4|^6.0", "symfony/config": "^5.4|^6.0", "symfony/console": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", "symfony/http-foundation": "^5.4|^6.0", "symfony/http-kernel": "^5.4|^6.0", "symfony/intl": "^5.4|^6.0",