8000 [FrameworkBundle] Integrate the HtmlSanitizer component by tgalopin · Pull Request #44798 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FrameworkBundle] Integrate the HtmlSanitizer component #44798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
< EDBE td class="blob-code blob-code-addition js-file-line"> ->end()
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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()
;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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\\',
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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')])
;
};
Loading
0