diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 5511672345465..3d4408153b969 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -25,6 +25,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Form\Form; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\Lock\Lock; @@ -167,6 +168,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addNotifierSection($rootNode, $enableIfStandalone); $this->addRateLimiterSection($rootNode, $enableIfStandalone); $this->addUidSection($rootNode, $enableIfStandalone); + $this->addHtmlSanitizerSection($rootNode, $enableIfStandalone); return $treeBuilder; } @@ -2106,4 +2108,147 @@ private function addUidSection(ArrayNodeDefinition $rootNode, callable $enableIf ->end() ; } + + private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) + { + $rootNode + ->children() + ->arrayNode('html_sanitizer') + ->info('HtmlSanitizer configuration') + ->{$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() + ->fixXmlConfig('allow_element') + ->fixXmlConfig('block_element') + ->fixXmlConfig('drop_element') + ->fixXmlConfig('allow_attribute') + ->fixXmlConfig('drop_attribute') + ->fixXmlConfig('force_attribute') + ->fixXmlConfig('allowed_link_scheme') + ->fixXmlConfig('allowed_link_host') + ->fixXmlConfig('allowed_media_scheme') + ->fixXmlConfig('allowed_media_host') + ->fixXmlConfig('with_attribute_sanitizer') + ->fixXmlConfig('without_attribute_sanitizer') + ->children() + ->booleanNode('allow_safe_elements') + ->info('Allows "safe" elements and attributes.') + ->defaultFalse() + ->end() + ->booleanNode('allow_all_static_elements') + ->info('Allows all static elements and attributes from the W3C Sanitizer API standard.') + ->defaultFalse() + ->end() + ->arrayNode('allow_elements') + ->info('Configures the elements that the sanitizer should retain from the input. The element name is the key, the value is either a list of allowed attributes for this element or "*" to allow the default set of attributes (https://wicg.github.io/sanitizer-api/#default-configuration).') + ->example(['i' => '*', 'a' => ['title'], 'span' => 'class']) + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->variablePrototype() + ->beforeNormalization() + ->ifArray()->then(fn ($n) => $n['attribute'] ?? $n) + ->end() + ->validate() + ->ifTrue(fn ($n): bool => !\is_string($n) && !\is_array($n)) + ->thenInvalid('The value must be either a string or an array of strings.') + ->end() + ->end() + ->end() + ->arrayNode('block_elements') + ->info('Configures elements as blocked. Blocked elements are elements the sanitizer should remove from the input, but retain their children.') + ->beforeNormalization() + ->ifString() + ->then(fn (string $n): array => (array) $n) + ->end() + ->scalarPrototype()->end() + ->end() + ->arrayNode('drop_elements') + ->info('Configures elements as dropped. Dropped elements are elements the sanitizer should remove from the input, including their children.') + ->beforeNormalization() + ->ifString() + ->then(fn (string $n): array => (array) $n) + ->end() + ->scalarPrototype()->end() + ->end() + ->arrayNode('allow_attributes') + ->info('Configures attributes as allowed. Allowed attributes are attributes the sanitizer should retain from the input.') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->variablePrototype() + ->beforeNormalization() + ->ifArray()->then(fn ($n) => $n['element'] ?? $n) + ->end() + ->end() + ->end() + ->arrayNode('drop_attributes') + ->info('Configures attributes as dropped. Dropped attributes are attributes the sanitizer should remove from the input.') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->variablePrototype() + ->beforeNormalization() + ->ifArray()->then(fn ($n) => $n['element'] ?? $n) + ->end() + ->end() + ->end() + ->arrayNode('force_attributes') + ->info('Forcefully set the values of certain attributes on certain elements.') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->arrayPrototype() + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->scalarPrototype()->end() + ->end() + ->end() + ->booleanNode('force_https_urls') + ->info('Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.') + ->defaultFalse() + ->end() + ->arrayNode('allowed_link_schemes') + ->info('Allows only a given list of schemes to be used in links href attributes.') + ->scalarPrototype()->end() + ->end() + ->arrayNode('allowed_link_hosts') + ->info('Allows only a given list of hosts to be used in links href attributes.') + ->scalarPrototype()->end() + ->end() + ->booleanNode('allow_relative_links') + ->info('Allows relative URLs to be used in links href attributes.') + ->defaultFalse() + ->end() + ->arrayNode('allowed_media_schemes') + ->info('Allows only a given list of schemes to be used in media source attributes (img, audio, video, ...).') + ->scalarPrototype()->end() + ->end() + ->arrayNode('allowed_media_hosts') + ->info('Allows only a given list of hosts to be used in media source attributes (img, audio, video, ...).') + ->scalarPrototype()->end() + ->end() + ->booleanNode('allow_relative_medias') + ->info('Allows relative URLs to be used in media source attributes (img, audio, video, ...).') + ->defaultFalse() + ->end() + ->arrayNode('with_attribute_sanitizers') + ->info('Registers custom attribute sanitizers.') + ->scalarPrototype()->end() + ->end() + ->arrayNode('without_attribute_sanitizers') + ->info('Unregisters custom attribute sanitizers.') + ->scalarPrototype()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index aeed63946b25a..af64c9bc807db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -73,6 +73,9 @@ use Symfony\Component\Form\FormTypeExtensionInterface; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; use Symfony\Component\HttpClient\RetryableHttpClient; @@ -531,6 +534,14 @@ public function load(array $configs, ContainerBuilder $container) // profiler depends on form, validation, translation, messenger, mailer, http-client, notifier, serializer being registered $this->registerProfilerConfiguration($config['profiler'], $container, $loader); + if ($this->isConfigEnabled($container, $config['html_sanitizer'])) { + if (!class_exists(HtmlSanitizerConfig::class)) { + throw new LogicException('HtmlSanitizer support cannot be enabled as the HtmlSanitizer component is not installed. Try running "composer require symfony/html-sanitizer".'); + } + + $this->registerHtmlSanitizerConfiguration($config['html_sanitizer'], $container, $loader); + } + $this->addAnnotatedClassesToCompile([ '**\\Controller\\', '**\\Entity\\', @@ -2659,6 +2670,81 @@ private function registerUidConfiguration(array $config, ContainerBuilder $conta } } + private function registerHtmlSanitizerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) + { + $loader->load('html_sanitizer.php'); + + foreach ($config['sanitizers'] as $sanitizerName => $sanitizerConfig) { + $configId = 'html_sanitizer.config.'.$sanitizerName; + $def = $container->register($configId, HtmlSanitizerConfig::class); + + // Base + if ($sanitizerConfig['allow_safe_elements']) { + $def->addMethodCall('allowSafeElements', [], true); + } + + if ($sanitizerConfig['allow_all_static_elements']) { + $def->addMethodCall('allowAllStaticElements', [], true); + } + + // Configures elements + foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) { + $def->addMethodCall('allowElement', [$element, $attributes], true); + } + + foreach ($sanitizerConfig['block_elements'] as $element) { + $def->addMethodCall('blockElement', [$element], true); + } + + foreach ($sanitizerConfig['drop_elements'] as $element) { + $def->addMethodCall('dropElement', [$element], true); + } + + // Configures attributes + foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) { + $def->addMethodCall('allowAttribute', [$attribute, $elements], true); + } + + foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) { + $def->addMethodCall('dropAttribute', [$attribute, $elements], true); + } + + // Force attributes + foreach ($sanitizerConfig['force_attributes'] as $element => $attributes) { + foreach ($attributes as $attrName => $attrValue) { + $def->addMethodCall('forceAttribute', [$element, $attrName, $attrValue], true); + } + } + + // Settings + $def->addMethodCall('forceHttpsUrls', [$sanitizerConfig['force_https_urls']], true); + $def->addMethodCall('allowLinkSchemes', [$sanitizerConfig['allowed_link_schemes']], true); + $def->addMethodCall('allowLinkHosts', [$sanitizerConfig['allowed_link_hosts']], true); + $def->addMethodCall('allowRelativeLinks', [$sanitizerConfig['allow_relative_links']], true); + $def->addMethodCall('allowMediaSchemes', [$sanitizerConfig['allowed_media_schemes']], true); + $def->addMethodCall('allowMediaHosts', [$sanitizerConfig['allowed_media_hosts']], true); + $def->addMethodCall('allowRelativeMedias', [$sanitizerConfig['allow_relative_medias']], true); + + // Custom attribute sanitizers + foreach ($sanitizerConfig['with_attribute_sanitizers'] as $serviceName) { + $def->addMethodCall('withAttributeSanitizer', [new Reference($serviceName)], true); + } + + foreach ($sanitizerConfig['without_attribute_sanitizers'] as $serviceName) { + $def->addMethodCall('withoutAttributeSanitizer', [new Reference($serviceName)], true); + } + + // Create the sanitizer and link its config + $sanitizerId = 'html_sanitizer.sanitizer.'.$sanitizerName; + $container->register($sanitizerId, HtmlSanitizer::class)->addArgument(new Reference($configId)); + + $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 { $trustedHeaders = 0; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php new file mode 100644 index 0000000000000..558188d18915f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/html_sanitizer.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('html_sanitizer.config', HtmlSanitizerConfig::class) + ->call('allowSafeElements') + + ->set('html_sanitizer', HtmlSanitizer::class) + ->args([service('html_sanitizer.config')]) + ; +}; 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 f932e596b9b7e..3ffa5b571503b 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 @@ -39,6 +39,7 @@ + @@ -819,4 +820,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index cf313d9a5f9ec..07612df737190 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; @@ -639,6 +640,11 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'name_based_uuid_version' => 5, 'time_based_uuid_version' => 6, ], + '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/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index 113324790d7ad..fd04b996da496 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -80,4 +80,7 @@ 'pdf' => 'application/pdf', ], ], + 'html_sanitizer' => [ + 'enabled' => true, + ], ]); 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 new file mode 100644 index 0000000000000..e7b1bd41fc360 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php @@ -0,0 +1,47 @@ +loadFromExtension('framework', [ + 'html_sanitizer' => [ + 'default' => 'my.sanitizer', + 'sanitizers' => [ + 'my.sanitizer' => [ + 'allow_safe_elements' => true, + 'allow_all_static_elements' => true, + 'allow_elements' => [ + 'iframe' => 'src', + 'custom-tag' => ['data-attr', 'data-attr-1'], + 'custom-tag-2' => '*', + ], + 'block_elements' => ['section'], + 'drop_elements' => ['video'], + 'allow_attributes' => [ + 'src' => ['iframe'], + 'data-attr' => '*', + ], + 'drop_attributes' => [ + 'data-attr' => ['custom-tag'], + 'data-attr-1' => [], + 'data-attr-2' => '*', + ], + 'force_attributes' => [ + 'a' => ['rel' => 'noopener noreferrer'], + 'h1' => ['class' => 'bp4-heading'], + ], + 'force_https_urls' => true, + 'allowed_link_schemes' => ['http', 'https', 'mailto'], + 'allowed_link_hosts' => ['symfony.com'], + 'allow_relative_links' => true, + 'allowed_media_schemes' => ['http', 'https', 'data'], + 'allowed_media_hosts' => ['symfony.com'], + 'allow_relative_medias' => true, + 'with_attribute_sanitizers' => [ + 'App\\Sanitizer\\CustomAttributeSanitizer', + ], + 'without_attribute_sanitizers' => [ + 'App\\Sanitizer\\OtherCustomAttributeSanitizer', + ], + ], + 'all.sanitizer' => null, + ], + ], +]); 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 new file mode 100644 index 0000000000000..05cf704dd2c6c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml @@ -0,0 +1,63 @@ + + + + + + + + + src + + + data-attr + data-attr-1 + + + * + + section + video + + iframe + + + * + + + custom-tag + + + + * + + + noopener noreferrer + + + bp4-heading + + http + https + mailto + symfony.com + http + https + data + symfony.com + App\Sanitizer\CustomAttributeSanitizer + App\Sanitizer\OtherCustomAttributeSanitizer + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml new file mode 100644 index 0000000000000..1c4f5dfcd5a4b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml @@ -0,0 +1,40 @@ +framework: + html_sanitizer: + default: my.sanitizer + sanitizers: + my.sanitizer: + allow_safe_elements: true + allow_all_static_elements: true + allow_elements: + iframe: 'src' + custom-tag: ['data-attr', 'data-attr-1'] + custom-tag-2: '*' + block_elements: + - section + drop_elements: + - video + allow_attributes: + src: ['iframe'] + data-attr: '*' + drop_attributes: + data-attr: [custom-tag] + data-attr-1: [] + data-attr-2: '*' + force_attributes: + a: + rel: noopener noreferrer + h1: + class: bp4-heading + force_https_urls: true + allowed_link_schemes: ['http', 'https', 'mailto'] + allowed_link_hosts: ['symfony.com'] + allow_relative_links: true + allowed_media_schemes: ['http', 'https', 'data'] + allowed_media_hosts: ['symfony.com'] + allow_relative_medias: true + with_attribute_sanitizers: + - App\Sanitizer\CustomAttributeSanitizer + without_attribute_sanitizers: + - App\Sanitizer\OtherCustomAttributeSanitizer + + all.sanitizer: null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index ba19f50419ede..e99bd83ba2028 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -47,6 +47,9 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Form\Form; +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; @@ -2027,6 +2030,80 @@ public function testLocaleSwitcherServiceRegistered() $this->assertEquals(new Reference('router.request_context', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $switcherDef->getArgument(2)); } + public function testHtmlSanitizer() + { + $container = $this->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->assertSame( + [ + ['allowSafeElements', [], true], + ['allowAllStaticElements', [], true], + ['allowElement', ['iframe', 'src'], true], + ['allowElement', ['custom-tag', ['data-attr', 'data-attr-1']], true], + ['allowElement', ['custom-tag-2', '*'], true], + ['blockElement', ['section'], true], + ['dropElement', ['video'], true], + ['allowAttribute', ['src', $this instanceof XmlFrameworkExtensionTest ? 'iframe' : ['iframe']], true], + ['allowAttribute', ['data-attr', '*'], true], + ['dropAttribute', ['data-attr', $this instanceof XmlFrameworkExtensionTest ? 'custom-tag' : ['custom-tag']], true], + ['dropAttribute', ['data-attr-1', []], true], + ['dropAttribute', ['data-attr-2', '*'], true], + ['forceAttribute', ['a', 'rel', 'noopener noreferrer'], true], + ['forceAttribute', ['h1', 'class', 'bp4-heading'], true], + ['forceHttpsUrls', [true], true], + ['allowLinkSchemes', [['http', 'https', 'mailto']], true], + ['allowLinkHosts', [['symfony.com']], true], + ['allowRelativeLinks', [true], true], + ['allowMediaSchemes', [['http', 'https', 'data']], true], + ['allowMediaHosts', [['symfony.com']], true], + ['allowRelativeMedias', [true], true], + ['withAttributeSanitizer', ['@App\\Sanitizer\\CustomAttributeSanitizer'], true], + ['withoutAttributeSanitizer', ['@App\\Sanitizer\\OtherCustomAttributeSanitizer'], true], + ], + + // Convert references to their names for easier assertion + array_map( + static function ($call) { + foreach ($call[1] as $k => $arg) { + $call[1][$k] = $arg instanceof Reference ? '@'.$arg : $arg; + } + + return $call; + }, + $calls + ) + ); + + // 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'); + + // Default alias + $this->assertSame('html_sanitizer.sanitizer.my.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class), '->registerHtmlSanitizerConfiguration() creates appropriate default alias'); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index c515fc557ce71..8a44eff41ac86 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -44,6 +44,7 @@ "symfony/polyfill-intl-icu": "~1.0", "symfony/form": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", "symfony/http-client": "^5.4|^6.0", "symfony/lock": "^5.4|^6.0", "symfony/mailer": "^5.4|^6.0",