10000 feature #44798 [FrameworkBundle] Integrate the HtmlSanitizer componen… · symfony/symfony@8064a5c · GitHub
[go: up one dir, main page]

Skip to content

Commit 8064a5c

Browse files
committed
feature #44798 [FrameworkBundle] Integrate the HtmlSanitizer component (tgalopin, wouterj)
This PR was merged into the 6.1 branch. Discussion ---------- [FrameworkBundle] Integrate the HtmlSanitizer component | Q | A | ------------- | --- | Branch? | 6.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This PR adds the integration if the HtmlSanitizer component in the FrameworkBundle. See #44681 for details about the component. The configuration for this integration is the following: ```yaml framework: # This configuration is not required: as soon as you install the component, a default # "html_sanitizer" service is created, with the safe configuration, to be used directly. # # This configuration allows to set custom behaviors, in addition or instead of the default. html_sanitizer: # Default sanitizer (optional) # When not provided, the native "html_sanitizer" service is wired as default. default: my.sanitizer # Custom sanitizers (optional) sanitizers: # Each sanitizer defines its own service (no prefix/suffix) to ease understanding # Each sanitizer also defines a named autowiring alias to ease injection using variable name. # Here, this sanitizer is available as a service "my.sanitizer" or using autowiring # as "HtmlSanitizerInterface $mySanitizer". my.sanitizer: allow_safe_elements: true allow_elements: iframe: ['src'] custom-tag: ['data-attr'] custom-tag-2: '*' block_elements: - section drop_elements: - video 10000 allow_attributes: src: ['iframe'] data-attr: '*' drop_attributes: data-attr: '*' 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 # "all.sanitizer" / "HtmlSanitizerInterface $allSanitizer" all.sanitizer: allow_all_static_elements: true allow_elements: custom-tag: ['data-attr'] ``` This PR is still WIP (esp tests) but I wanted to gather feedback regarding the configuration and DX as soon as possible. Commits ------- 4dd3fd6 Finished XML config implementation e0a9339 Integrate the HtmlSanitizer component to the FrameworkBundle
2 parents ed382fc + 4dd3fd6 commit 8064a5c

File tree

11 files changed

+554
-0
lines changed

11 files changed

+554
-0
lines changed

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

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\DependencyInjection\ContainerBuilder;
2626
use Symfony\Component\DependencyInjection\Exception\LogicException;
2727
use Symfony\Component\Form\Form;
28+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
2829
use Symfony\Component\HttpClient\HttpClient;
2930
use Symfony\Component\HttpFoundation\Cookie;
3031
use Symfony\Component\Lock\Lock;
@@ -167,6 +168,7 @@ public function getConfigTreeBuilder(): TreeBuilder
167168
$this->addNotifierSection($rootNode, $enableIfStandalone);
168169
$this->addRateLimiterSection($rootNode, $enableIfStandalone);
169170
$this->addUidSection($rootNode, $enableIfStandalone);
171+
$this->addHtmlSanitizerSection($rootNode, $enableIfStandalone);
170172

171173
return $treeBuilder;
172174
}
@@ -2106,4 +2108,147 @@ private function addUidSection(ArrayNodeDefinition $rootNode, callable $enableIf
21062108
->end()
21072109
;
21082110
}
2111+
2112+
private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
2113+
{
2114+
$rootNode
2115+
->children()
2116+
->arrayNode('html_sanitizer')
2117+
->info('HtmlSanitizer configuration')
2118+
->{$enableIfStandalone('symfony/html-sanitizer', HtmlSanitizerInterface::class)}()
2119+
->fixXmlConfig('sanitizer')
2120+
->children()
2121+
->scalarNode('default')
2122+
->defaultNull()
2123+
->info('Default sanitizer to use when injecting without named binding.')
2124+
->end()
2125+
->arrayNode('sanitizers')
2126+
->useAttributeAsKey('name')
2127+
->arrayPrototype()
2128+
->fixXmlConfig('allow_element')
2129+
->fixXmlConfig('block_element')
2130+
->fixXmlConfig('drop_element')
2131+
->fixXmlConfig('allow_attribute')
2132+
->fixXmlConfig('drop_attribute')
2133+
->fixXmlConfig('force_attribute')
2134+
->fixXmlConfig('allowed_link_scheme')
2135+
->fixXmlConfig('allowed_link_host')
2136+
->fixXmlConfig('allowed_media_scheme')
2137+
->fixXmlConfig('allowed_media_host')
2138+
->fixXmlConfig('with_attribute_sanitizer')
2139+
->fixXmlConfig('without_attribute_sanitizer')
2140+
->children()
2141+
->booleanNode('allow_safe_elements')
2142+
->info('Allows "safe" elements and attributes.')
2143+
->defaultFalse()
2144+
->end()
2145+
->booleanNode('allow_all_static_elements')
2146+
->info('Allows all static elements and attributes from the W3C Sanitizer API standard.')
2147+
->defaultFalse()
2148+
->end()
2149+
->arrayNode('allow_elements')
2150+
->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).')
2151+
->example(['i' => '*', 'a' => ['title'], 'span' => 'class'])
2152+
->normalizeKeys(false)
2153+
->useAttributeAsKey('name')
2154+
->variablePrototype()
2155+
->beforeNormalization()
2156+
->ifArray()->then(fn ($n) => $n['attribute'] ?? $n)
2157+
->end()
2158+
->validate()
2159+
->ifTrue(fn ($n): bool => !\is_string($n) && !\is_array($n))
2160+
->thenInvalid('The value must be either a string or an array of strings.')
2161+
->end()
2162+
->end()
2163+
->end()
2164+
->arrayNode('block_elements')
2165+
->info('Configures elements as blocked. Blocked elements are elements the sanitizer should remove from the input, but retain their children.')
2166+
->beforeNormalization()
2167+
->ifString()
2168+
->then(fn (string $n): array => (array) $n)
2169+
->end()
2170+
->scalarPrototype()->end()
2171+
->end()
2172+
->arrayNode('drop_elements')
2173+
->info('Configures elements as dropped. Dropped elements are elements the sanitizer should remove from the input, including their children.')
2174+
->beforeNormalization()
2175+
->ifString()
2176+
->then(fn (string $n): array => (array) $n)
2177+
->end()
2178+
->scalarPrototype()->end()
2179+
->end()
2180+
->arrayNode('allow_attributes')
2181+
->info('Configures attributes as allowed. Allowed attributes are attributes the sanitizer should retain from the input.')
2182+
->normalizeKeys(false)
2183+
->useAttributeAsKey('name')
2184+
->variablePrototype()
2185+
->beforeNormalization()
2186+
->ifArray()->then(fn ($n) => $n['element'] ?? $n)
2187+
->end()
2188+
->end()
2189+
->end()
2190+
->arrayNode('drop_attributes')
2191+
->info('Configures attributes as dropped. Dropped attributes are attributes the sanitizer should remove from the input.')
2192+
->normalizeKeys(false)
2193+
->useAttributeAsKey('name')
2194+
->variablePrototype()
2195+
->beforeNormalization()
2196+
->ifArray()->then(fn ($n) => $n['element'] ?? $n)
2197+
->end()
2198+
->end()
2199+
->end()
2200+
->arrayNode('force_attributes')
2201+
->info('Forcefully set the values of certain attributes on certain elements.')
2202+
->normalizeKeys(false)
2203+
->useAttributeAsKey('name')
2204+
->arrayPrototype()
2205+
->normalizeKeys(false)
2206+
->useAttributeAsKey('name')
2207+
->scalarPrototype()->end()
2208+
->end()
2209+
->end()
2210+
->booleanNode('force_https_urls')
2211+
->info('Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.')
2212+
->defaultFalse()
2213+
->end()
2214+
->arrayNode('allowed_link_schemes')
2215+
->info('Allows only a given list of schemes to be used in links href attributes.')
2216+
->scalarPrototype()->end()
2217+
->end()
2218+
->arrayNode('allowed_link_hosts')
2219+
->info('Allows only a given list of hosts to be used in links href attributes.')
2220+
->scalarPrototype()->end()
2221+
->end()
2222+
->booleanNode('allow_relative_links')
2223+
->info('Allows relative URLs to be used in links href attributes.')
2224+
->defaultFalse()
2225+
->end()
2226+
->arrayNode('allowed_media_schemes')
2227+
->info('Allows only a given list of schemes to be used in media source attributes (img, audio, video, ...).')
2228+
->scalarPrototype()->end()
2229+
->end()
2230+
->arrayNode('allowed_media_hosts')
2231+
->info('Allows only a given list of hosts to be used in media source attributes (img, audio, video, ...).')
2232+
->scalarPrototype()->end()
2233+
->end()
2234+
->booleanNode('allow_relative_medias')
2235+
->info('Allows relative URLs to be used in media source attributes (img, audio, video, ...).')
2236+
->defaultFalse()
2237+
->end()
2238+
->arrayNode('with_attribute_sanitizers')
2239+
->info('Registers custom attribute sanitizers.')
2240+
->scalarPrototype()->end()
2241+
->end()
2242+
->arrayNode('without_attribute_sanitizers')
2243+
->info('Unregisters custom attribute sanitizers.')
2244+
->scalarPrototype()->end()
2245+
->end()
2246+
->end()
2247+
->end()
2248+
->end()
2249+
->end()
2250+
->end()
2251+
->end()
2252+
;
2253+
}
21092254
}

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
use Symfony\Component\Form\FormTypeExtensionInterface;
7474
use Symfony\Component\Form\FormTypeGuesserInterface;
7575
use Symfony\Component\Form\FormTypeInterface;
76+
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
77+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
78+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
7679
use Symfony\Component\HttpClient\MockHttpClient;
7780
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
7881
use Symfony\Component\HttpClient\RetryableHttpClient;
@@ -531,6 +534,14 @@ public function load(array $configs, ContainerBuilder $container)
531534
// profiler depends on form, validation, translation, messenger, mailer, http-client, notifier, serializer being registered
532535
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
533536

537+
if ($this->isConfigEnabled($container, $config['html_sanitizer'])) {
538+
if (!class_exists(HtmlSanitizerConfig::class)) {
539+
throw new LogicException('HtmlSanitizer support cannot be enabled as the HtmlSanitizer component is not installed. Try running "composer require symfony/html-sanitizer".');
540+
}
541+
542+
$this->registerHtmlSanitizerConfiguration($config['html_sanitizer'], $container, $loader);
543+
}
544+
534545
$this->addAnnotatedClassesToCompile([
535546
'**\\Controller\\',
536547
'**\\Entity\\',
@@ -2659,6 +2670,81 @@ private function registerUidConfiguration(array $config, ContainerBuilder $conta
26592670
}
26602671
}
26612672

2673+
private function registerHtmlSanitizerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2674+
{
2675+
$loader->load('html_sanitizer.php');
2676+
2677+
foreach ($config['sanitizers'] as $sanitizerName => $sanitizerConfig) {
2678+
$configId = 'html_sanitizer.config.'.$sanitizerName;
2679+
$def = $container->register($configId, HtmlSanitizerConfig::class);
2680+
2681+
// Base
2682+
if ($sanitizerConfig['allow_safe_elements']) {
2683+
$def->addMethodCall('allowSafeElements', [], true);
2684+
}
2685+
2686+
if ($sanitizerConfig['allow_all_static_elements']) {
2687+
$def->addMethodCall('allowAllStaticElements', [], true);
2688+
}
2689+
2690+
// Configures elements
2691+
foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) {
2692+
$def->addMethodCall('allowElement', [$element, $attributes], true);
2693+
}
2694+
2695+
foreach ($sanitizerConfig['block_elements'] as $element) {
2696+
$def->addMethodCall('blockElement', [$element], true);
2697+
}
2698+
2699+
foreach ($sanitizerConfig['drop_elements'] as $element) {
2700+
$def->addMethodCall('dropElement', [$element], true);
2701+
}
2702+
2703+
// Configures attributes
2704+
foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) {
2705+
$def->addMethodCall('allowAttribute', [$attribute, $elements], true);
2706+
}
2707+
2708+
foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) {
2709+
$def->addMethodCall('dropAttribute', [$attribute, $elements], true);
2710+
}
2711+
2712+
// Force attributes
2713+
foreach ($sanitizerConfig['force_attributes'] as $element => $attributes) {
2714+
foreach ($attributes as $attrName => $attrValue) {
2715+
$def->addMethodCall('forceAttribute', [$element, $attrName, $attrValue], true);
2716+
}
2717+
}
2718+
2719+
// Settings
2720+
$def->addMethodCall('forceHttpsUrls', [$sanitizerConfig['force_https_urls']], true);
2721+
$def->addMethodCall('allowLinkSchemes', [$sanitizerConfig['allowed_link_schemes']], true);
2722+
$def->addMethodCall('allowLinkHosts', [$sanitizerConfig['allowed_link_hosts']], true);
2723+
$def->addMethodCall('allowRelativeLinks', [$sanitizerConfig['allow_relative_links']], true);
2724+
$def->addMethodCall('allowMediaSchemes', [$sanitizerConfig['allowed_media_schemes']], true);
2725+
$def->addMethodCall('allowMediaHosts', [$sanitizerConfig['allowed_media_hosts']], true);
2726+
$def->addMethodCall('allowRelativeMedias', [$sanitizerConfig['allow_relative_medias']], true);
2727+
2728+
// Custom attribute sanitizers
2729+
foreach ($sanitizerConfig['with_attribute_sanitizers'] as $serviceName) {
2730+
$def->addMethodCall('withAttributeSanitizer', [new Reference($serviceName)], true);
2731+
}
2732+
2733+
foreach ($sanitizerConfig['without_attribute_sanitizers'] as $serviceName) {
2734+
$def->addMethodCall('withoutAttributeSanitizer', [new Reference($serviceName)], true);
2735+
}
2736+
2737+
// Create the sanitizer and link its config
2738+
$sanitizerId = 'html_sanitizer.sanitizer.'.$sanitizerName;
2739+
$container->register($sanitizerId, HtmlSanitizer::class)->addArgument(new Reference($configId));
2740+
2741+
$container->registerAliasForArgument($sanitizerId, HtmlSanitizerInterface::class, $sanitizerName);
2742+
}
2743+
2744+
$default = $config['default'] ? 'html_sanitizer.sanitizer.'.$config['default'] : 'html_sanitizer';
2745+
$container->setAlias(HtmlSanitizerInterface::class, new Reference($default));
2746+
}
2747+
26622748
private function resolveTrustedHeaders(array $headers): int
26632749
{
26642750
$trustedHeaders = 0;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
15+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('html_sanitizer.config', HtmlSanitizerConfig::class)
20+
->call('allowSafeElements')
21+
22+
->set('html_sanitizer', HtmlSanitizer::class)
23+
->args([service('html_sanitizer.config')])
24+
;
25+
};

0 commit comments

Comments
 (0)
0