refactor: Cleanup git state - commit all staged changes
Major refactoring cleanup: - Add new controller architecture (class-controller-*.php) - Add new settings-v2 UI (views/settings-v2/) - Add new CSS architecture (agentic-sidebar.css, tokens) - Add esbuild build pipeline (scripts/build.js, package.json) - Add composer dependencies (vendor/) - Add frontend src directory (assets/js/src/index.jsx) - Add documentation files - Remove old/obsolete files (class-settings.php, old CSS) This commits all pending changes from previous refactoring efforts.
This commit is contained in:
31
vendor/symfony/html-sanitizer/CHANGELOG.md
vendored
Normal file
31
vendor/symfony/html-sanitizer/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
* Remove `MastermindsParser`; use `NativeParser` instead
|
||||
* Add argument `$context` to `ParserInterface::parse()`
|
||||
|
||||
7.4
|
||||
---
|
||||
|
||||
* Use the native HTML5 parser when using PHP 8.4+
|
||||
* Deprecate `MastermindsParser`; use `NativeParser` instead
|
||||
* [BC BREAK] `ParserInterface::parse()` can now return `\Dom\Node|\DOMNode|null` instead of just `\DOMNode|null`
|
||||
* Add argument `$context` to `ParserInterface::parse()`
|
||||
|
||||
7.2
|
||||
---
|
||||
|
||||
* Add support for configuring the default action to block or allow unconfigured elements instead of dropping them
|
||||
|
||||
6.4
|
||||
---
|
||||
|
||||
* Add support for sanitizing unlimited length of HTML document
|
||||
|
||||
6.1
|
||||
---
|
||||
|
||||
* Add the component as experimental
|
||||
135
vendor/symfony/html-sanitizer/HtmlSanitizer.php
vendored
Normal file
135
vendor/symfony/html-sanitizer/HtmlSanitizer.php
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
<?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\HtmlSanitizer;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\Parser\NativeParser;
|
||||
use Symfony\Component\HtmlSanitizer\Parser\ParserInterface;
|
||||
use Symfony\Component\HtmlSanitizer\Reference\W3CReference;
|
||||
use Symfony\Component\HtmlSanitizer\TextSanitizer\StringSanitizer;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\DomVisitor;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
final class HtmlSanitizer implements HtmlSanitizerInterface
|
||||
{
|
||||
private ParserInterface $parser;
|
||||
|
||||
/**
|
||||
* @var array<string, DomVisitor>
|
||||
*/
|
||||
private array $domVisitors = [];
|
||||
|
||||
public function __construct(
|
||||
private HtmlSanitizerConfig $config,
|
||||
?ParserInterface $parser = null,
|
||||
) {
|
||||
$this->parser = $parser ?? new NativeParser();
|
||||
}
|
||||
|
||||
public function sanitize(string $input): string
|
||||
{
|
||||
return $this->sanitizeFor(W3CReference::CONTEXT_BODY, $input);
|
||||
}
|
||||
|
||||
public function sanitizeFor(string $element, string $input): string
|
||||
{
|
||||
$element = StringSanitizer::htmlLower($element);
|
||||
$context = W3CReference::CONTEXTS_MAP[$element] ?? W3CReference::CONTEXT_BODY;
|
||||
$element = isset(W3CReference::BODY_ELEMENTS[$element]) ? $element : $context;
|
||||
|
||||
// Prevent DOS attack induced by extremely long HTML strings
|
||||
if (-1 !== $this->config->getMaxInputLength() && \strlen($input) > $this->config->getMaxInputLength()) {
|
||||
$input = substr($input, 0, $this->config->getMaxInputLength());
|
||||
}
|
||||
|
||||
// Text context: early return with HTML encoding
|
||||
if (W3CReference::CONTEXT_TEXT === $context) {
|
||||
return StringSanitizer::encodeHtmlEntities($input);
|
||||
}
|
||||
|
||||
// Other context: build a DOM visitor
|
||||
$this->domVisitors[$context] ??= $this->createDomVisitorForContext($context);
|
||||
|
||||
// Only operate on valid UTF-8 strings. This is necessary to prevent cross
|
||||
// site scripting issues on Internet Explorer 6. Idea from Drupal (filter_xss).
|
||||
if (!$this->isValidUtf8($input)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove NULL character and HTML entities for null byte
|
||||
$input = str_replace(\chr(0), '<27>', $input);
|
||||
|
||||
// Parse as HTML
|
||||
if ('' === trim($input) || !$parsed = $this->parser->parse($input, $element)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Visit the DOM tree and render the sanitized nodes
|
||||
return $this->domVisitors[$context]->visit($parsed)?->render() ?? '';
|
||||
}
|
||||
|
||||
private function isValidUtf8(string $html): bool
|
||||
{
|
||||
// preg_match() fails silently on strings containing invalid UTF-8.
|
||||
return '' === $html || preg_match('//u', $html);
|
||||
}
|
||||
|
||||
private function createDomVisitorForContext(string $context): DomVisitor
|
||||
{
|
||||
$elementsConfig = [];
|
||||
|
||||
// Head: only a few elements are allowed
|
||||
if (W3CReference::CONTEXT_HEAD === $context) {
|
||||
foreach ($this->config->getAllowedElements() as $allowedElement => $allowedAttributes) {
|
||||
if (\array_key_exists($allowedElement, W3CReference::HEAD_ELEMENTS)) {
|
||||
$elementsConfig[$allowedElement] = $allowedAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->config->getBlockedElements() as $blockedElement => $v) {
|
||||
if (\array_key_exists($blockedElement, W3CReference::HEAD_ELEMENTS)) {
|
||||
$elementsConfig[$blockedElement] = HtmlSanitizerAction::Block;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->config->getDroppedElements() as $droppedElement => $v) {
|
||||
if (\array_key_exists($droppedElement, W3CReference::HEAD_ELEMENTS)) {
|
||||
$elementsConfig[$droppedElement] = HtmlSanitizerAction::Drop;
|
||||
}
|
||||
}
|
||||
|
||||
return new DomVisitor($this->config, $elementsConfig);
|
||||
}
|
||||
|
||||
// Body: allow any configured element that isn't in <head>
|
||||
foreach ($this->config->getAllowedElements() as $allowedElement => $allowedAttributes) {
|
||||
if (!\array_key_exists($allowedElement, W3CReference::HEAD_ELEMENTS)) {
|
||||
$elementsConfig[$allowedElement] = $allowedAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->config->getBlockedElements() as $blockedElement => $v) {
|
||||
if (!\array_key_exists($blockedElement, W3CReference::HEAD_ELEMENTS)) {
|
||||
$elementsConfig[$blockedElement] = HtmlSanitizerAction::Block;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->config->getDroppedElements() as $droppedElement => $v) {
|
||||
if (!\array_key_exists($droppedElement, W3CReference::HEAD_ELEMENTS)) {
|
||||
$elementsConfig[$droppedElement] = HtmlSanitizerAction::Drop;
|
||||
}
|
||||
}
|
||||
|
||||
return new DomVisitor($this->config, $elementsConfig);
|
||||
}
|
||||
}
|
||||
30
vendor/symfony/html-sanitizer/HtmlSanitizerAction.php
vendored
Normal file
30
vendor/symfony/html-sanitizer/HtmlSanitizerAction.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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\HtmlSanitizer;
|
||||
|
||||
enum HtmlSanitizerAction: string
|
||||
{
|
||||
/**
|
||||
* Dropped elements are elements the sanitizer should remove from the input, including their children.
|
||||
*/
|
||||
case Drop = 'drop';
|
||||
|
||||
/**
|
||||
* Blocked elements are elements the sanitizer should remove from the input, but retain their children.
|
||||
*/
|
||||
case Block = 'block';
|
||||
|
||||
/**
|
||||
* Allowed elements are elements the sanitizer should retain from the input.
|
||||
*/
|
||||
case Allow = 'allow';
|
||||
}
|
||||
553
vendor/symfony/html-sanitizer/HtmlSanitizerConfig.php
vendored
Normal file
553
vendor/symfony/html-sanitizer/HtmlSanitizerConfig.php
vendored
Normal file
@@ -0,0 +1,553 @@
|
||||
<?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\HtmlSanitizer;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\Reference\W3CReference;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\AttributeSanitizer\AttributeSanitizerInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class HtmlSanitizerConfig
|
||||
{
|
||||
private HtmlSanitizerAction $defaultAction = HtmlSanitizerAction::Drop;
|
||||
|
||||
/**
|
||||
* Elements that should be removed.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $droppedElements = [];
|
||||
|
||||
/**
|
||||
* Elements that should be removed but their children should be retained.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $blockedElements = [];
|
||||
|
||||
/**
|
||||
* Elements that should be retained, with their allowed attributes.
|
||||
*
|
||||
* @var array<string, array<string, true>>
|
||||
*/
|
||||
private array $allowedElements = [];
|
||||
|
||||
/**
|
||||
* Attributes that should always be added to certain elements.
|
||||
*
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private array $forcedAttributes = [];
|
||||
|
||||
/**
|
||||
* Links schemes that should be retained, other being dropped.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $allowedLinkSchemes = ['http', 'https', 'mailto', 'tel'];
|
||||
|
||||
/**
|
||||
* Links hosts that should be retained (by default, all hosts are allowed).
|
||||
*
|
||||
* @var list<string>|null
|
||||
*/
|
||||
private ?array $allowedLinkHosts = null;
|
||||
|
||||
/**
|
||||
* Should the sanitizer allow relative links (by default, they are dropped).
|
||||
*/
|
||||
private bool $allowRelativeLinks = false;
|
||||
|
||||
/**
|
||||
* Image/Audio/Video schemes that should be retained, other being dropped.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $allowedMediaSchemes = ['http', 'https', 'data'];
|
||||
|
||||
/**
|
||||
* Image/Audio/Video hosts that should be retained (by default, all hosts are allowed).
|
||||
*
|
||||
* @var list<string>|null
|
||||
*/
|
||||
private ?array $allowedMediaHosts = null;
|
||||
|
||||
/**
|
||||
* Should the sanitizer allow relative media URL (by default, they are dropped).
|
||||
*/
|
||||
private bool $allowRelativeMedias = false;
|
||||
|
||||
/**
|
||||
* Should the URL in the sanitized document be transformed to HTTPS if they are using HTTP.
|
||||
*/
|
||||
private bool $forceHttpsUrls = false;
|
||||
|
||||
/**
|
||||
* Sanitizers that should be applied to specific attributes in addition to standard sanitization.
|
||||
*
|
||||
* @var list<AttributeSanitizerInterface>
|
||||
*/
|
||||
private array $attributeSanitizers;
|
||||
|
||||
private int $maxInputLength = 20_000;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->attributeSanitizers = [
|
||||
new Visitor\AttributeSanitizer\UrlAttributeSanitizer(),
|
||||
new Visitor\AttributeSanitizer\MetaRefreshAttributeSanitizer(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default action for elements which are not otherwise specifically allowed or blocked.
|
||||
*
|
||||
* Note that a default action of Allow will allow all tags but they will not have any attributes.
|
||||
*/
|
||||
public function defaultAction(HtmlSanitizerAction $action): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->defaultAction = $action;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows all static elements and attributes from the W3C Sanitizer API standard.
|
||||
*
|
||||
* All scripts will be removed but the output may still contain other dangerous
|
||||
* behaviors like CSS injection (click-jacking), CSS expressions, ...
|
||||
*/
|
||||
public function allowStaticElements(): static
|
||||
{
|
||||
$elements = array_merge(
|
||||
array_keys(W3CReference::HEAD_ELEMENTS),
|
||||
array_keys(W3CReference::BODY_ELEMENTS)
|
||||
);
|
||||
|
||||
$clone = clone $this;
|
||||
foreach ($elements as $element) {
|
||||
$clone = $clone->allowElement($element, '*');
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows "safe" elements and attributes.
|
||||
*
|
||||
* All scripts will be removed, as well as other dangerous behaviors like CSS injection.
|
||||
*/
|
||||
public function allowSafeElements(): static
|
||||
{
|
||||
$attributes = [];
|
||||
foreach (W3CReference::ATTRIBUTES as $attribute => $isSafe) {
|
||||
if ($isSafe) {
|
||||
$attributes[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
|
||||
foreach (W3CReference::HEAD_ELEMENTS as $element => $isSafe) {
|
||||
if ($isSafe) {
|
||||
$clone = $clone->allowElement($element, $attributes);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (W3CReference::BODY_ELEMENTS as $element => $isSafe) {
|
||||
if ($isSafe) {
|
||||
$clone = $clone->allowElement($element, $attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows only a given list of schemes to be used in links href attributes.
|
||||
*
|
||||
* All other schemes will be dropped.
|
||||
*
|
||||
* @param list<string> $allowLinkSchemes
|
||||
*/
|
||||
public function allowLinkSchemes(array $allowLinkSchemes): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->allowedLinkSchemes = $allowLinkSchemes;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows only a given list of hosts to be used in links href attributes.
|
||||
*
|
||||
* All other hosts will be dropped. By default all hosts are allowed
|
||||
* ($allowedLinkHosts = null).
|
||||
*
|
||||
* @param list<string>|null $allowLinkHosts
|
||||
*/
|
||||
public function allowLinkHosts(?array $allowLinkHosts): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->allowedLinkHosts = $allowLinkHosts;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows relative URLs to be used in links href attributes.
|
||||
*/
|
||||
public function allowRelativeLinks(bool $allowRelativeLinks = true): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->allowRelativeLinks = $allowRelativeLinks;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows only a given list of schemes to be used in media source attributes (img, audio, video, ...).
|
||||
*
|
||||
* All other schemes will be dropped.
|
||||
*
|
||||
* @param list<string> $allowMediaSchemes
|
||||
*/
|
||||
public function allowMediaSchemes(array $allowMediaSchemes): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->allowedMediaSchemes = $allowMediaSchemes;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows only a given list of hosts to be used in media source attributes (img, audio, video, ...).
|
||||
*
|
||||
* All other hosts will be dropped. By default all hosts are allowed
|
||||
* ($allowMediaHosts = null).
|
||||
*
|
||||
* @param list<string>|null $allowMediaHosts
|
||||
*/
|
||||
public function allowMediaHosts(?array $allowMediaHosts): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->allowedMediaHosts = $allowMediaHosts;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows relative URLs to be used in media source attributes (img, audio, video, ...).
|
||||
*/
|
||||
public function allowRelativeMedias(bool $allowRelativeMedias = true): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->allowRelativeMedias = $allowRelativeMedias;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.
|
||||
*/
|
||||
public function forceHttpsUrls(bool $forceHttpsUrls = true): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->forceHttpsUrls = $forceHttpsUrls;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given element as allowed.
|
||||
*
|
||||
* Allowed elements are elements the sanitizer should retain from the input.
|
||||
*
|
||||
* A list of allowed attributes for this element can be passed as a second argument.
|
||||
* Passing "*" will allow all standard attributes on this element. By default, no
|
||||
* attributes are allowed on the element.
|
||||
*
|
||||
* @param list<string>|string $allowedAttributes
|
||||
*/
|
||||
public function allowElement(string $element, array|string $allowedAttributes = []): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
|
||||
// Unblock/undrop the element if necessary
|
||||
unset($clone->blockedElements[$element], $clone->droppedElements[$element]);
|
||||
|
||||
$clone->allowedElements[$element] = [];
|
||||
|
||||
$attrs = ('*' === $allowedAttributes) ? array_keys(W3CReference::ATTRIBUTES) : (array) $allowedAttributes;
|
||||
foreach ($attrs as $allowedAttr) {
|
||||
$clone->allowedElements[$element][$allowedAttr] = true;
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given element as blocked.
|
||||
*
|
||||
* Blocked elements are elements the sanitizer should remove from the input, but retain
|
||||
* their children.
|
||||
*/
|
||||
public function blockElement(string $element): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
|
||||
// Disallow/undrop the element if necessary
|
||||
unset($clone->allowedElements[$element], $clone->droppedElements[$element]);
|
||||
|
||||
$clone->blockedElements[$element] = true;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given element as dropped.
|
||||
*
|
||||
* Dropped elements are elements the sanitizer should remove from the input, including
|
||||
* their children.
|
||||
*
|
||||
* Note: when using an empty configuration, all unknown elements are dropped
|
||||
* automatically. This method let you drop elements that were allowed earlier
|
||||
* in the configuration, or explicitly drop some if you changed the default action.
|
||||
*/
|
||||
public function dropElement(string $element): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
unset($clone->allowedElements[$element], $clone->blockedElements[$element]);
|
||||
|
||||
$clone->droppedElements[$element] = true;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given attribute as allowed.
|
||||
*
|
||||
* Allowed attributes are attributes the sanitizer should retain from the input.
|
||||
*
|
||||
* A list of allowed elements for this attribute can be passed as a second argument.
|
||||
* Passing "*" will allow all currently allowed elements to use this attribute.
|
||||
*
|
||||
* Note: this method is subtractive within the currently allowed elements.
|
||||
* It restricts the attribute to the listed elements and removes it from any
|
||||
* other allowed element that previously had it. To add an attribute to one
|
||||
* element without affecting others, use allowElement($element, [$attribute]).
|
||||
*
|
||||
* @param list<string>|string $allowedElements
|
||||
*/
|
||||
public function allowAttribute(string $attribute, array|string $allowedElements): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$allowedElements = ('*' === $allowedElements) ? array_keys($clone->allowedElements) : (array) $allowedElements;
|
||||
|
||||
// For each configured element ...
|
||||
foreach ($clone->allowedElements as $element => $attrs) {
|
||||
if (\in_array($element, $allowedElements, true)) {
|
||||
// ... if the attribute should be allowed, add it
|
||||
$clone->allowedElements[$element][$attribute] = true;
|
||||
} else {
|
||||
// ... if the attribute should not be allowed, remove it
|
||||
unset($clone->allowedElements[$element][$attribute]);
|
||||
}
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given attribute as dropped.
|
||||
*
|
||||
* Dropped attributes are attributes the sanitizer should remove from the input.
|
||||
*
|
||||
* A list of elements on which to drop this attribute can be passed as a second argument.
|
||||
* Passing "*" will drop this attribute from all currently allowed elements.
|
||||
*
|
||||
* Note: when using an empty configuration, all unknown attributes are dropped
|
||||
* automatically. This method let you drop attributes that were allowed earlier
|
||||
* in the configuration.
|
||||
*
|
||||
* @param list<string>|string $droppedElements
|
||||
*/
|
||||
public function dropAttribute(string $attribute, array|string $droppedElements): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$droppedElements = ('*' === $droppedElements) ? array_keys($clone->allowedElements) : (array) $droppedElements;
|
||||
|
||||
foreach ($droppedElements as $element) {
|
||||
if (isset($clone->allowedElements[$element][$attribute])) {
|
||||
unset($clone->allowedElements[$element][$attribute]);
|
||||
}
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcefully set the value of a given attribute on a given element.
|
||||
*
|
||||
* The attribute will be created on the nodes if it didn't exist. The
|
||||
* provided value is written verbatim and is NOT passed through any
|
||||
* attribute sanitizer (in particular, URL attribute sanitization is
|
||||
* skipped), so callers are responsible for ensuring the value is safe.
|
||||
*/
|
||||
public function forceAttribute(string $element, string $attribute, string $value): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->forcedAttributes[$element][$attribute] = $value;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom attribute sanitizer.
|
||||
*/
|
||||
public function withAttributeSanitizer(AttributeSanitizerInterface $sanitizer): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->attributeSanitizers[] = $sanitizer;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a custom attribute sanitizer.
|
||||
*/
|
||||
public function withoutAttributeSanitizer(AttributeSanitizerInterface $sanitizer): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->attributeSanitizers = array_values(array_filter(
|
||||
$this->attributeSanitizers,
|
||||
static fn ($current) => $current !== $sanitizer
|
||||
));
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxInputLength The maximum length of the input string in bytes
|
||||
* -1 means no limit
|
||||
*/
|
||||
public function withMaxInputLength(int $maxInputLength): static
|
||||
{
|
||||
if ($maxInputLength < -1) {
|
||||
throw new \InvalidArgumentException(\sprintf('The maximum input length must be greater than -1, "%d" given.', $maxInputLength));
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->maxInputLength = $maxInputLength;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function getMaxInputLength(): int
|
||||
{
|
||||
return $this->maxInputLength;
|
||||
}
|
||||
|
||||
public function getDefaultAction(): HtmlSanitizerAction
|
||||
{
|
||||
return $this->defaultAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, true>>
|
||||
*/
|
||||
public function getAllowedElements(): array
|
||||
{
|
||||
return $this->allowedElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, true>
|
||||
*/
|
||||
public function getBlockedElements(): array
|
||||
{
|
||||
return $this->blockedElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, true>
|
||||
*/
|
||||
public function getDroppedElements(): array
|
||||
{
|
||||
return $this->droppedElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
public function getForcedAttributes(): array
|
||||
{
|
||||
return $this->forcedAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getAllowedLinkSchemes(): array
|
||||
{
|
||||
return $this->allowedLinkSchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function getAllowedLinkHosts(): ?array
|
||||
{
|
||||
return $this->allowedLinkHosts;
|
||||
}
|
||||
|
||||
public function getAllowRelativeLinks(): bool
|
||||
{
|
||||
return $this->allowRelativeLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getAllowedMediaSchemes(): array
|
||||
{
|
||||
return $this->allowedMediaSchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function getAllowedMediaHosts(): ?array
|
||||
{
|
||||
return $this->allowedMediaHosts;
|
||||
}
|
||||
|
||||
public function getAllowRelativeMedias(): bool
|
||||
{
|
||||
return $this->allowRelativeMedias;
|
||||
}
|
||||
|
||||
public function getForceHttpsUrls(): bool
|
||||
{
|
||||
return $this->forceHttpsUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<AttributeSanitizerInterface>
|
||||
*/
|
||||
public function getAttributeSanitizers(): array
|
||||
{
|
||||
return $this->attributeSanitizers;
|
||||
}
|
||||
}
|
||||
42
vendor/symfony/html-sanitizer/HtmlSanitizerInterface.php
vendored
Normal file
42
vendor/symfony/html-sanitizer/HtmlSanitizerInterface.php
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\HtmlSanitizer;
|
||||
|
||||
/**
|
||||
* Sanitizes an untrusted HTML input for safe insertion into a document's DOM.
|
||||
*
|
||||
* This interface is inspired by the W3C Standard Draft about a HTML Sanitizer API
|
||||
* ({@see https://wicg.github.io/sanitizer-api/}).
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
interface HtmlSanitizerInterface
|
||||
{
|
||||
/**
|
||||
* Sanitizes an untrusted HTML input for a <body> context.
|
||||
*
|
||||
* This method is NOT context sensitive: it assumes the returned HTML string
|
||||
* will be injected in a "body" context, and therefore will drop tags only
|
||||
* allowed in the "head" element. To sanitize a string for injection
|
||||
* in the "head" element, use {@see HtmlSanitizerInterface::sanitizeFor()}.
|
||||
*/
|
||||
public function sanitize(string $input): string;
|
||||
|
||||
/**
|
||||
* Sanitizes an untrusted HTML input for a given context.
|
||||
*
|
||||
* This method is context sensitive: by providing a parent element name
|
||||
* (body, head, title, ...), the sanitizer will adapt its rules to only
|
||||
* allow elements that are valid inside the given parent element.
|
||||
*/
|
||||
public function sanitizeFor(string $element, string $input): string;
|
||||
}
|
||||
19
vendor/symfony/html-sanitizer/LICENSE
vendored
Normal file
19
vendor/symfony/html-sanitizer/LICENSE
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
26
vendor/symfony/html-sanitizer/Parser/NativeParser.php
vendored
Normal file
26
vendor/symfony/html-sanitizer/Parser/NativeParser.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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\HtmlSanitizer\Parser;
|
||||
|
||||
/**
|
||||
* Parser using PHP 8.4's new Dom API.
|
||||
*/
|
||||
final class NativeParser implements ParserInterface
|
||||
{
|
||||
public function parse(string $html, string $context = 'body'): ?\Dom\Node
|
||||
{
|
||||
$document = @\Dom\HTMLDocument::createFromString(\sprintf('<!DOCTYPE html><%s>%s</%1$s>', $context, $html));
|
||||
$element = $document->getElementsByTagName($context)->item(0);
|
||||
|
||||
return $element->hasChildNodes() ? $element : null;
|
||||
}
|
||||
}
|
||||
29
vendor/symfony/html-sanitizer/Parser/ParserInterface.php
vendored
Normal file
29
vendor/symfony/html-sanitizer/Parser/ParserInterface.php
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\HtmlSanitizer\Parser;
|
||||
|
||||
/**
|
||||
* Transforms an untrusted HTML input string into a DOM tree.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* Parse a given string and returns a DOMNode tree.
|
||||
*
|
||||
* This method must return null if the string cannot be parsed as HTML.
|
||||
*
|
||||
* @param string $context The name of the context element in which the HTML is parsed
|
||||
*/
|
||||
public function parse(string $html, string $context = 'body'): \Dom\Node|\DOMNode|null;
|
||||
}
|
||||
125
vendor/symfony/html-sanitizer/README.md
vendored
Normal file
125
vendor/symfony/html-sanitizer/README.md
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
HtmlSanitizer Component
|
||||
=======================
|
||||
|
||||
The HtmlSanitizer component provides an object-oriented API to sanitize
|
||||
untrusted HTML input for safe insertion into a document's DOM.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
```php
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
|
||||
|
||||
// By default, an element not added to the allowed or blocked elements
|
||||
// will be dropped, including its children
|
||||
$config = (new HtmlSanitizerConfig())
|
||||
// Allow "safe" elements and attributes. All scripts will be removed
|
||||
// as well as other dangerous behaviors like CSS injection
|
||||
->allowSafeElements()
|
||||
|
||||
// Allow all static elements and attributes from the W3C Sanitizer API
|
||||
// standard. All scripts will be removed but the output may still contain
|
||||
// other dangerous behaviors like CSS injection (click-jacking), CSS
|
||||
// expressions, ...
|
||||
->allowStaticElements()
|
||||
|
||||
// Allow the "div" element and no attribute can be on it
|
||||
->allowElement('div')
|
||||
|
||||
// Allow the "a" element, and the "title" attribute to be on it
|
||||
->allowElement('a', ['title'])
|
||||
|
||||
// Allow the "span" element, and any attribute from the Sanitizer API is allowed
|
||||
// (see https://wicg.github.io/sanitizer-api/#default-configuration)
|
||||
->allowElement('span', '*')
|
||||
|
||||
// Block the "section" element: this element will be removed but
|
||||
// its children will be retained
|
||||
->blockElement('section')
|
||||
|
||||
// Drop the "div" element: this element will be removed, including its children
|
||||
->dropElement('div')
|
||||
|
||||
// Allow the attribute "title" on the "div" element
|
||||
->allowAttribute('title', ['div'])
|
||||
|
||||
// Allow the attribute "data-custom-attr" on all currently allowed elements
|
||||
->allowAttribute('data-custom-attr', '*')
|
||||
|
||||
// Drop the "data-custom-attr" attribute from the "div" element:
|
||||
// this attribute will be removed
|
||||
->dropAttribute('data-custom-attr', ['div'])
|
||||
|
||||
// Drop the "data-custom-attr" attribute from all elements:
|
||||
// this attribute will be removed
|
||||
->dropAttribute('data-custom-attr', '*')
|
||||
|
||||
// Forcefully set the value of all "rel" attributes on "a"
|
||||
// elements to "noopener noreferrer"
|
||||
->forceAttribute('a', 'rel', 'noopener noreferrer')
|
||||
|
||||
// Transform all HTTP schemes to HTTPS
|
||||
->forceHttpsUrls()
|
||||
|
||||
// Configure which schemes are allowed in links (others will be dropped)
|
||||
->allowLinkSchemes(['https', 'http', 'mailto'])
|
||||
|
||||
// Configure which hosts are allowed in links (by default all are allowed)
|
||||
->allowLinkHosts(['symfony.com', 'example.com'])
|
||||
|
||||
// Allow relative URL in links (by default they are dropped)
|
||||
->allowRelativeLinks()
|
||||
|
||||
// Configure which schemes are allowed in img/audio/video/iframe (others will be dropped)
|
||||
->allowMediaSchemes(['https', 'http'])
|
||||
|
||||
// Configure which hosts are allowed in img/audio/video/iframe (by default all are allowed)
|
||||
->allowMediaHosts(['symfony.com', 'example.com'])
|
||||
|
||||
// Allow relative URL in img/audio/video/iframe (by default they are dropped)
|
||||
->allowRelativeMedias()
|
||||
|
||||
// Configure a custom attribute sanitizer to apply custom sanitization logic
|
||||
// ($attributeSanitizer instance of AttributeSanitizerInterface)
|
||||
->withAttributeSanitizer($attributeSanitizer)
|
||||
|
||||
// Unregister a previously registered attribute sanitizer
|
||||
// ($attributeSanitizer instance of AttributeSanitizerInterface)
|
||||
->withoutAttributeSanitizer($attributeSanitizer)
|
||||
;
|
||||
|
||||
$sanitizer = new HtmlSanitizer($config);
|
||||
|
||||
// Sanitize a given string, using the configuration provided and in the
|
||||
// "body" context (tags only allowed in <head> will be removed)
|
||||
$sanitizer->sanitize($userInput);
|
||||
|
||||
// Sanitize the given string for a usage in a <head> tag
|
||||
$sanitizer->sanitizeFor('head', $userInput);
|
||||
|
||||
// Sanitize the given string for a usage in another tag
|
||||
$sanitizer->sanitizeFor('title', $userInput); // Will encode as HTML entities
|
||||
$sanitizer->sanitizeFor('textarea', $userInput); // Will encode as HTML entities
|
||||
$sanitizer->sanitizeFor('div', $userInput); // Will sanitize as body
|
||||
$sanitizer->sanitizeFor('section', $userInput); // Will sanitize as body
|
||||
// ...
|
||||
```
|
||||
|
||||
Sponsor
|
||||
-------
|
||||
|
||||
This package is looking for a [backer][1].
|
||||
|
||||
Help Symfony by [sponsoring][3] its development!
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
||||
[1]: https://symfony.com/backers
|
||||
[3]: https://symfony.com/sponsor
|
||||
400
vendor/symfony/html-sanitizer/Reference/W3CReference.php
vendored
Normal file
400
vendor/symfony/html-sanitizer/Reference/W3CReference.php
vendored
Normal file
@@ -0,0 +1,400 @@
|
||||
<?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\HtmlSanitizer\Reference;
|
||||
|
||||
/**
|
||||
* Stores reference data from the W3C Sanitizer API standard.
|
||||
*
|
||||
* @see https://wicg.github.io/sanitizer-api/#default-configuration
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class W3CReference
|
||||
{
|
||||
/**
|
||||
* Sanitizer supported contexts.
|
||||
*
|
||||
* A parent element name can be passed as an argument to {@see HtmlSanitizer::sanitizeFor()}.
|
||||
* When doing so, depending on the given context, different elements will be allowed.
|
||||
*/
|
||||
public const CONTEXT_HEAD = 'head';
|
||||
public const CONTEXT_BODY = 'body';
|
||||
public const CONTEXT_TEXT = 'text';
|
||||
|
||||
// Which context to apply depending on the passed parent element name
|
||||
public const CONTEXTS_MAP = [
|
||||
'head' => self::CONTEXT_HEAD,
|
||||
'textarea' => self::CONTEXT_TEXT,
|
||||
'title' => self::CONTEXT_TEXT,
|
||||
];
|
||||
|
||||
/**
|
||||
* Elements allowed by the Sanitizer standard in <head> as keys, including whether
|
||||
* they are safe or not as values (safe meaning no global display/audio/video impact).
|
||||
*/
|
||||
public const HEAD_ELEMENTS = [
|
||||
'head' => true,
|
||||
'link' => true,
|
||||
'meta' => true,
|
||||
'style' => false,
|
||||
'title' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Elements allowed by the Sanitizer standard in <body> as keys, including whether
|
||||
* they are safe or not as values (safe meaning no global display/audio/video impact).
|
||||
*/
|
||||
public const BODY_ELEMENTS = [
|
||||
'a' => true,
|
||||
'abbr' => true,
|
||||
'acronym' => true,
|
||||
'address' => true,
|
||||
'area' => true,
|
||||
'article' => true,
|
||||
'aside' => true,
|
||||
'audio' => true,
|
||||
'b' => true,
|
||||
'basefont' => true,
|
||||
'bdi' => true,
|
||||
'bdo' => true,
|
||||
'bgsound' => false,
|
||||
'big' => true,
|
||||
'blockquote' => true,
|
||||
'body' => true,
|
||||
'br' => true,
|
||||
'button' => true,
|
||||
'canvas' => true,
|
||||
'caption' => true,
|
||||
'center' => true,
|
||||
'cite' => true,
|
||||
'code' => true,
|
||||
'col' => true,
|
||||
'colgroup' => true,
|
||||
'command' => true,
|
||||
'data' => true,
|
||||
'datalist' => true,
|
||||
'dd' => true,
|
||||
'del' => true,
|
||||
'details' => true,
|
||||
'dfn' => true,
|
||||
'dialog' => true,
|
||||
'dir' => true,
|
||||
'div' => true,
|
||||
'dl' => true,
|
||||
'dt' => true,
|
||||
'em' => true,
|
||||
'fieldset' => true,
|
||||
'figcaption' => true,
|
||||
'figure' => true,
|
||||
'font' => true,
|
||||
'footer' => true,
|
||||
'form' => false,
|
||||
'h1' => true,
|
||||
'h2' => true,
|
||||
'h3' => true,
|
||||
'h4' => true,
|
||||
'h5' => true,
|
||||
'h6' => true,
|
||||
'header' => true,
|
||||
'hgroup' => true,
|
||||
'hr' => true,
|
||||
'html' => true,
|
||||
'i' => true,
|
||||
'image' => true,
|
||||
'img' => true,
|
||||
'input' => false,
|
||||
'ins' => true,
|
||||
'kbd' => true,
|
||||
'keygen' => true,
|
||||
'label' => true,
|
||||
'layer' => true,
|
||||
'legend' => true,
|
||||
'li' => true,
|
||||
'listing' => true,
|
||||
'main' => true,
|
||||
'map' => true,
|
||||
'mark' => true,
|
||||
'marquee' => true,
|
||||
'menu' => true,
|
||||
'meter' => true,
|
||||
'nav' => true,
|
||||
'nobr' => true,
|
||||
'ol' => true,
|
||||
'optgroup' => true,
|
||||
'option' => true,
|
||||
'output' => true,
|
||||
'p' => true,
|
||||
'picture' => true,
|
||||
'plaintext' => true,
|
||||
'popup' => true,
|
||||
'portal' => true,
|
||||
'pre' => true,
|
||||
'progress' => true,
|
||||
'q' => true,
|
||||
'rb' => true,
|
||||
'rp' => true,
|
||||
'rt' => true,
|
||||
'rtc' => true,
|
||||
'ruby' => true,
|
||||
's' => true,
|
||||
'samp' => true,
|
||||
'section' => true,
|
||||
'select' => false,
|
||||
'selectmenu' => false,
|
||||
'slot' => true,
|
||||
'small' => true,
|
||||
'source' => true,
|
||||
'span' => true,
|
||||
'strike' => true,
|
||||
'strong' => true,
|
||||
'sub' => true,
|
||||
'summary' => true,
|
||||
'sup' => true,
|
||||
'table' => true,
|
||||
'tbody' => true,
|
||||
'td' => true,
|
||||
'template' => true,
|
||||
'textarea' => false,
|
||||
'tfoot' => true,
|
||||
'th' => true,
|
||||
'thead' => true,
|
||||
'time' => true,
|
||||
'tr' => true,
|
||||
'track' => true,
|
||||
'tt' => true,
|
||||
'u' => true,
|
||||
'ul' => true,
|
||||
'var' => true,
|
||||
'video' => true,
|
||||
'wbr' => true,
|
||||
'xmp' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Attributes allowed by the standard.
|
||||
*/
|
||||
public const ATTRIBUTES = [
|
||||
'abbr' => true,
|
||||
'accept' => true,
|
||||
'accept-charset' => true,
|
||||
'accesskey' => true,
|
||||
'action' => true,
|
||||
'align' => true,
|
||||
'alink' => true,
|
||||
'allow' => true,
|
||||
'allowfullscreen' => true,
|
||||
'allowpaymentrequest' => false,
|
||||
'alt' => true,
|
||||
'anchor' => true,
|
||||
'archive' => true,
|
||||
'as' => true,
|
||||
'async' => false,
|
||||
'autocapitalize' => false,
|
||||
'autocomplete' => false,
|
||||
'autocorrect' => false,
|
||||
'autofocus' => false,
|
||||
'autopictureinpicture' => false,
|
||||
'autoplay' => false,
|
||||
'axis' => true,
|
||||
'background' => false,
|
||||
'behavior' => true,
|
||||
'bgcolor' => false,
|
||||
'border' => false,
|
||||
'bordercolor' => false,
|
||||
'capture' => true,
|
||||
'cellpadding' => true,
|
||||
'cellspacing' => true,
|
||||
'challenge' => true,
|
||||
'char' => true,
|
||||
'charoff' => true,
|
||||
'charset' => true,
|
||||
'checked' => false,
|
||||
'cite' => true,
|
||||
'class' => false,
|
||||
'classid' => false,
|
||||
'clear' => true,
|
||||
'code' => true,
|
||||
'codebase' => true,
|
||||
'codetype' => true,
|
||||
'color' => false,
|
||||
'cols' => true,
|
||||
'colspan' => true,
|
||||
'compact' => true,
|
||||
'content' => true,
|
||||
'contenteditable' => false,
|
||||
'controls' => true,
|
||||
'controlslist' => true,
|
||||
'conversiondestination' => true,
|
||||
'coords' => true,
|
||||
'crossorigin' => true,
|
||||
'csp' => true,
|
||||
'data' => true,
|
||||
'datetime' => true,
|
||||
'declare' => true,
|
||||
'decoding' => true,
|
||||
'default' => true,
|
||||
'defer' => true,
|
||||
'dir' => true,
|
||||
'direction' => true,
|
||||
'dirname' => true,
|
||||
'disabled' => true,
|
||||
'disablepictureinpicture' => true,
|
||||
'disableremoteplayback' => true,
|
||||
'disallowdocumentaccess' => true,
|
||||
'download' => true,
|
||||
'draggable' => true,
|
||||
'elementtiming' => true,
|
||||
'enctype' => true,
|
||||
'end' => true,
|
||||
'enterkeyhint' => true,
|
||||
'event' => true,
|
||||
'exportparts' => true,
|
||||
'face' => true,
|
||||
'for' => true,
|
||||
'form' => false,
|
||||
'formaction' => false,
|
||||
'formenctype' => false,
|
||||
'formmethod' => false,
|
||||
'formnovalidate' => false,
|
||||
'formtarget' => false,
|
||||
'frame' => false,
|
||||
'frameborder' => false,
|
||||
'headers' => true,
|
||||
'height' => true,
|
||||
'hidden' => false,
|
||||
'high' => true,
|
||||
'href' => true,
|
||||
'hreflang' => true,
|
||||
'hreftranslate' => true,
|
||||
'hspace' => true,
|
||||
'http-equiv' => false,
|
||||
'id' => true,
|
||||
'imagesizes' => true,
|
||||
'imagesrcset' => true,
|
||||
'importance' => true,
|
||||
'impressiondata' => true,
|
||||
'impressionexpiry' => true,
|
||||
'incremental' => true,
|
||||
'inert' => true,
|
||||
'inputmode' => true,
|
||||
'integrity' => true,
|
||||
'invisible' => true,
|
||||
'is' => true,
|
||||
'ismap' => true,
|
||||
'keytype' => true,
|
||||
'kind' => true,
|
||||
'label' => true,
|
||||
'lang' => true,
|
||||
'language' => true,
|
||||
'latencyhint' => true,
|
||||
'leftmargin' => true,
|
||||
'link' => true,
|
||||
'list' => true,
|
||||
'loading' => true,
|
||||
'longdesc' => true,
|
||||
'loop' => true,
|
||||
'low' => true,
|
||||
'lowsrc' => true,
|
||||
'manifest' => true,
|
||||
'marginheight' => true,
|
||||
'marginwidth' => true,
|
||||
'max' => true,
|
||||
'maxlength' => true,
|
||||
'mayscript' => true,
|
||||
'media' => true,
|
||||
'method' => true,
|
||||
'min' => true,
|
||||
'minlength' => true,
|
||||
'multiple' => true,
|
||||
'muted' => true,
|
||||
'name' => true,
|
||||
'nohref' => true,
|
||||
'nomodule' => true,
|
||||
'nonce' => true,
|
||||
'noresize' => true,
|
||||
'noshade' => true,
|
||||
'novalidate' => true,
|
||||
'nowrap' => true,
|
||||
'object' => true,
|
||||
'open' => true,
|
||||
'optimum' => true,
|
||||
'part' => true,
|
||||
'pattern' => true,
|
||||
'ping' => false,
|
||||
'placeholder' => true,
|
||||
'playsinline' => true,
|
||||
'policy' => true,
|
||||
'poster' => true,
|
||||
'preload' => true,
|
||||
'pseudo' => true,
|
||||
'readonly' => true,
|
||||
'referrerpolicy' => true,
|
||||
'rel' => true,
|
||||
'reportingorigin' => true,
|
||||
'required' => true,
|
||||
'resources' => true,
|
||||
'rev' => true,
|
||||
'reversed' => true,
|
||||
'role' => true,
|
||||
'rows' => true,
|
||||
'rowspan' => true,
|
||||
'rules' => true,
|
||||
'sandbox' => true,
|
||||
'scheme' => true,
|
||||
'scope' => true,
|
||||
'scopes' => true,
|
||||
'scrollamount' => true,
|
||||
'scrolldelay' => true,
|
||||
'scrolling' => true,
|
||||
'select' => false,
|
||||
'selected' => false,
|
||||
'shadowroot' => true,
|
||||
'shadowrootdelegatesfocus' => true,
|
||||
'shape' => true,
|
||||
'size' => true,
|
||||
'sizes' => true,
|
||||
'slot' => true,
|
||||
'span' => true,
|
||||
'spellcheck' => true,
|
||||
'src' => true,
|
||||
// 'srcdoc' => false, // XSS vector if not properly sandboxed, should be enabled explicitly with ->allowAttribute('srcdoc', 'iframe')->forceAttribute('iframe', 'sandbox', '')
|
||||
'srclang' => true,
|
||||
'srcset' => true,
|
||||
'standby' => true,
|
||||
'start' => true,
|
||||
'step' => true,
|
||||
'style' => false,
|
||||
'summary' => true,
|
||||
'tabindex' => true,
|
||||
'target' => true,
|
||||
'text' => true,
|
||||
'title' => true,
|
||||
'topmargin' => true,
|
||||
'translate' => true,
|
||||
'truespeed' => true,
|
||||
'trusttoken' => true,
|
||||
'type' => true,
|
||||
'usemap' => true,
|
||||
'valign' => true,
|
||||
'value' => false,
|
||||
'valuetype' => true,
|
||||
'version' => true,
|
||||
'virtualkeyboardpolicy' => true,
|
||||
'vlink' => false,
|
||||
'vspace' => true,
|
||||
'webkitdirectory' => true,
|
||||
'width' => true,
|
||||
'wrap' => true,
|
||||
];
|
||||
}
|
||||
56
vendor/symfony/html-sanitizer/TextSanitizer/StringSanitizer.php
vendored
Normal file
56
vendor/symfony/html-sanitizer/TextSanitizer/StringSanitizer.php
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\HtmlSanitizer\TextSanitizer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StringSanitizer
|
||||
{
|
||||
private const REPLACEMENTS = [
|
||||
// """ is shorter than """
|
||||
'"' => '"',
|
||||
|
||||
// Fix several potential issues in how browsers interpret attribute values
|
||||
'+' => '+',
|
||||
'=' => '=',
|
||||
'@' => '@',
|
||||
'`' => '`',
|
||||
|
||||
// Some DB engines will transform UTF8 full-width characters with
|
||||
// their classical version if the data is saved in a non-UTF8 field
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'+' => '+',
|
||||
'=' => '=',
|
||||
'@' => '@',
|
||||
'`' => '`',
|
||||
];
|
||||
|
||||
/**
|
||||
* Applies a transformation to lowercase following W3C HTML Standard.
|
||||
*
|
||||
* @see https://w3c.github.io/html-reference/terminology.html#case-insensitive
|
||||
*/
|
||||
public static function htmlLower(string $string): string
|
||||
{
|
||||
return strtolower($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the HTML entities in the given string for safe injection in a document's DOM.
|
||||
*/
|
||||
public static function encodeHtmlEntities(string $string): string
|
||||
{
|
||||
return strtr(htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'), self::REPLACEMENTS);
|
||||
}
|
||||
}
|
||||
209
vendor/symfony/html-sanitizer/TextSanitizer/UrlSanitizer.php
vendored
Normal file
209
vendor/symfony/html-sanitizer/TextSanitizer/UrlSanitizer.php
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
<?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\HtmlSanitizer\TextSanitizer;
|
||||
|
||||
use League\Uri\Exceptions\SyntaxError;
|
||||
use League\Uri\UriString;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class UrlSanitizer
|
||||
{
|
||||
/**
|
||||
* Characters with no legitimate place in a URL: explicit-direction BiDi
|
||||
* formatting marks plus Unicode whitespace and the zero-width no-break
|
||||
* space. ASCII space is tolerated and percent-encoded by parse().
|
||||
*/
|
||||
private const DENIED_CHARS_PATTERN = '/[\t\n\v\f\r\x{0085}\x{00A0}\x{1680}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}\x{202A}-\x{202E}\x{2066}-\x{2069}]/u';
|
||||
|
||||
/**
|
||||
* Sanitizes a given URL string.
|
||||
*
|
||||
* In addition to ensuring $input is a valid URL, this sanitizer checks that:
|
||||
* * the URL's host is allowed ;
|
||||
* * the URL's scheme is allowed ;
|
||||
* * the URL is allowed to be relative if it is ;
|
||||
*
|
||||
* It also transforms the URL to HTTPS if requested.
|
||||
*/
|
||||
public static function sanitize(?string $input, ?array $allowedSchemes = null, bool $forceHttps = false, ?array $allowedHosts = null, bool $allowRelative = false): ?string
|
||||
{
|
||||
if (!$input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (false !== strpbrk($input, '\\') || preg_match('~^(?:https?|ftp|wss?):(/[^/]|///)~i', $input)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = self::parse($input);
|
||||
|
||||
// Malformed URL
|
||||
if (!$url || !\is_array($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// No scheme and relative not allowed
|
||||
if (!$allowRelative && !$url['scheme']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Forbidden scheme
|
||||
if ($url['scheme'] && null !== $allowedSchemes && !\in_array($url['scheme'], $allowedSchemes, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the scheme used is not supposed to have a host, do not check the host
|
||||
if (!self::isHostlessScheme($url['scheme'])) {
|
||||
// No host and relative not allowed
|
||||
if (!$allowRelative && !$url['host']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Forbidden host
|
||||
if ($url['host'] && null !== $allowedHosts && !self::isAllowedHost($url['host'], $allowedHosts)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Force HTTPS
|
||||
if ($forceHttps && 'http' === $url['scheme']) {
|
||||
$url['scheme'] = 'https';
|
||||
}
|
||||
|
||||
return UriString::build($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a given URL and returns an array of its components.
|
||||
*
|
||||
* @return array{
|
||||
* scheme:?string,
|
||||
* user:?string,
|
||||
* pass:?string,
|
||||
* host:?string,
|
||||
* port:?int,
|
||||
* path:string,
|
||||
* query:?string,
|
||||
* fragment:?string
|
||||
* }|null
|
||||
*/
|
||||
public static function parse(string $url): ?array
|
||||
{
|
||||
if (!$url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Reject explicit-direction BiDi formatting characters and non-space
|
||||
// whitespace: they have no legitimate place in a URL and enable
|
||||
// visual spoofing of the rendered href when the URL is later
|
||||
// embedded in HTML or decoded by a downstream consumer.
|
||||
if (preg_match(self::DENIED_CHARS_PATTERN, $url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Browsers tolerate spaces inside path/query/fragment by transparently
|
||||
// percent-encoding them. Mirror that behavior, but never inside the
|
||||
// scheme or authority (where spaces are illegal); the whitespace check
|
||||
// below rejects any space that didn't fit in the encoded slice.
|
||||
if (str_contains($url, ' ')) {
|
||||
if (str_starts_with($url, ' ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (false !== $i = strpos($url, '://')) {
|
||||
$i += 3 + strcspn($url, '/?#', $i + 3);
|
||||
} elseif (str_starts_with($url, '//')) {
|
||||
$i = 2 + strcspn($url, '/?#', 2);
|
||||
} elseif (preg_match('#^[a-z][a-z0-9+.\-]*:#i', $url)) {
|
||||
// Hostless scheme (data:, mailto:, …): leave the URL untouched
|
||||
// and let the whitespace check reject it.
|
||||
$i = \strlen($url);
|
||||
} else {
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
$url = substr($url, 0, $i).str_replace(' ', '%20', substr($url, $i));
|
||||
}
|
||||
|
||||
if (preg_match('/\s/', $url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parsedUrl = UriString::parse($url);
|
||||
|
||||
if (isset($parsedUrl['host']) && self::decodeUnreservedCharacters($parsedUrl['host']) !== $parsedUrl['host']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reject denied characters reachable via percent-encoding in any
|
||||
// component; otherwise the upfront check is bypassed by encoding.
|
||||
foreach (['user', 'pass', 'host', 'path', 'query', 'fragment'] as $part) {
|
||||
if (isset($parsedUrl[$part]) && preg_match(self::DENIED_CHARS_PATTERN, rawurldecode($parsedUrl[$part]))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $parsedUrl;
|
||||
} catch (SyntaxError) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static function isHostlessScheme(?string $scheme): bool
|
||||
{
|
||||
return \in_array($scheme, ['blob', 'chrome', 'data', 'file', 'geo', 'mailto', 'maps', 'tel', 'sms', 'view-source'], true);
|
||||
}
|
||||
|
||||
private static function isAllowedHost(?string $host, array $allowedHosts): bool
|
||||
{
|
||||
if (null === $host) {
|
||||
return \in_array(null, $allowedHosts, true);
|
||||
}
|
||||
|
||||
$parts = array_reverse(explode('.', $host));
|
||||
|
||||
foreach ($allowedHosts as $allowedHost) {
|
||||
if (self::matchAllowedHostParts($parts, array_reverse(explode('.', $allowedHost)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function matchAllowedHostParts(array $uriParts, array $trustedParts): bool
|
||||
{
|
||||
// Check each chunk of the domain is valid
|
||||
foreach ($trustedParts as $key => $trustedPart) {
|
||||
if (!\array_key_exists($key, $uriParts) || $uriParts[$key] !== $trustedPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation borrowed from League\Uri\Encoder::decodeUnreservedCharacters().
|
||||
*/
|
||||
private static function decodeUnreservedCharacters(string $host): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
',%(2[1-9A-Fa-f]|[3-7][0-9A-Fa-f]|61|62|64|65|66|7[AB]|5F),',
|
||||
static fn (array $matches): string => rawurldecode($matches[0]),
|
||||
$host
|
||||
);
|
||||
}
|
||||
}
|
||||
41
vendor/symfony/html-sanitizer/Visitor/AttributeSanitizer/AttributeSanitizerInterface.php
vendored
Normal file
41
vendor/symfony/html-sanitizer/Visitor/AttributeSanitizer/AttributeSanitizerInterface.php
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\HtmlSanitizer\Visitor\AttributeSanitizer;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||
|
||||
/**
|
||||
* Implements attribute-specific sanitization logic.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
interface AttributeSanitizerInterface
|
||||
{
|
||||
/**
|
||||
* Returns the list of element names supported, or null to support all elements.
|
||||
*
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function getSupportedElements(): ?array;
|
||||
|
||||
/**
|
||||
* Returns the list of attributes names supported, or null to support all attributes.
|
||||
*
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function getSupportedAttributes(): ?array;
|
||||
|
||||
/**
|
||||
* Returns the sanitized value of a given attribute for the given element.
|
||||
*/
|
||||
public function sanitizeAttribute(string $element, string $attribute, string $value, HtmlSanitizerConfig $config): ?string;
|
||||
}
|
||||
56
vendor/symfony/html-sanitizer/Visitor/AttributeSanitizer/MetaRefreshAttributeSanitizer.php
vendored
Normal file
56
vendor/symfony/html-sanitizer/Visitor/AttributeSanitizer/MetaRefreshAttributeSanitizer.php
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\HtmlSanitizer\Visitor\AttributeSanitizer;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||
use Symfony\Component\HtmlSanitizer\TextSanitizer\UrlSanitizer;
|
||||
|
||||
/**
|
||||
* Sanitizes the URL embedded in the content attribute of a <meta http-equiv="refresh">
|
||||
* element, since the http-equiv value is not visible from a per-attribute sanitizer.
|
||||
*
|
||||
* The content attribute carries an unrelated value for other meta types (description,
|
||||
* keywords, generator…), which is passed through unchanged.
|
||||
*/
|
||||
final class MetaRefreshAttributeSanitizer implements AttributeSanitizerInterface
|
||||
{
|
||||
public function getSupportedElements(): ?array
|
||||
{
|
||||
return ['meta'];
|
||||
}
|
||||
|
||||
public function getSupportedAttributes(): ?array
|
||||
{
|
||||
return ['content'];
|
||||
}
|
||||
|
||||
public function sanitizeAttribute(string $element, string $attribute, string $value, HtmlSanitizerConfig $config): ?string
|
||||
{
|
||||
if (!preg_match('/^(\s*\d+\s*[;,]\s*url\s*=\s*)(["\']?)(.+?)\2(\s*)$/i', $value, $m)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$sanitized = UrlSanitizer::sanitize(
|
||||
$m[3],
|
||||
$config->getAllowedLinkSchemes(),
|
||||
$config->getForceHttpsUrls(),
|
||||
$config->getAllowedLinkHosts(),
|
||||
$config->getAllowRelativeLinks(),
|
||||
);
|
||||
|
||||
if (null === $sanitized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $m[1].$m[2].$sanitized.$m[2].$m[4];
|
||||
}
|
||||
}
|
||||
53
vendor/symfony/html-sanitizer/Visitor/AttributeSanitizer/UrlAttributeSanitizer.php
vendored
Normal file
53
vendor/symfony/html-sanitizer/Visitor/AttributeSanitizer/UrlAttributeSanitizer.php
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\HtmlSanitizer\Visitor\AttributeSanitizer;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||
use Symfony\Component\HtmlSanitizer\TextSanitizer\UrlSanitizer;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
final class UrlAttributeSanitizer implements AttributeSanitizerInterface
|
||||
{
|
||||
public function getSupportedElements(): ?array
|
||||
{
|
||||
// Check all elements for URL attributes
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSupportedAttributes(): ?array
|
||||
{
|
||||
return ['src', 'href', 'lowsrc', 'background', 'ping', 'action', 'formaction', 'poster', 'cite', 'data', 'codebase', 'archive', 'longdesc'];
|
||||
}
|
||||
|
||||
public function sanitizeAttribute(string $element, string $attribute, string $value, HtmlSanitizerConfig $config): ?string
|
||||
{
|
||||
if (\in_array($element, ['a', 'area'], true) || \in_array($attribute, ['action', 'formaction', 'cite'], true)) {
|
||||
return UrlSanitizer::sanitize(
|
||||
$value,
|
||||
$config->getAllowedLinkSchemes(),
|
||||
$config->getForceHttpsUrls(),
|
||||
$config->getAllowedLinkHosts(),
|
||||
$config->getAllowRelativeLinks(),
|
||||
);
|
||||
}
|
||||
|
||||
return UrlSanitizer::sanitize(
|
||||
$value,
|
||||
$config->getAllowedMediaSchemes(),
|
||||
$config->getForceHttpsUrls(),
|
||||
$config->getAllowedMediaHosts(),
|
||||
$config->getAllowRelativeMedias(),
|
||||
);
|
||||
}
|
||||
}
|
||||
192
vendor/symfony/html-sanitizer/Visitor/DomVisitor.php
vendored
Normal file
192
vendor/symfony/html-sanitizer/Visitor/DomVisitor.php
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
<?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\HtmlSanitizer\Visitor;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction;
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||
use Symfony\Component\HtmlSanitizer\TextSanitizer\StringSanitizer;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\AttributeSanitizer\AttributeSanitizerInterface;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Model\Cursor;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Node\BlockedNode;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Node\DocumentNode;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Node\Node;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Node\NodeInterface;
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Node\TextNode;
|
||||
|
||||
/**
|
||||
* Iterates over the parsed DOM tree to build the sanitized tree.
|
||||
*
|
||||
* The DomVisitor iterates over the parsed DOM tree, visits its nodes and build
|
||||
* a sanitized tree with their attributes and content.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DomVisitor
|
||||
{
|
||||
private HtmlSanitizerAction $defaultAction = HtmlSanitizerAction::Drop;
|
||||
|
||||
/**
|
||||
* Registry of attributes to forcefully set on nodes, index by element and attribute.
|
||||
*
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private array $forcedAttributes;
|
||||
|
||||
/**
|
||||
* Registry of attributes sanitizers indexed by element name and attribute name for
|
||||
* faster sanitization.
|
||||
*
|
||||
* @var array<string, array<string, list<AttributeSanitizerInterface>>>
|
||||
*/
|
||||
private array $attributeSanitizers = [];
|
||||
|
||||
/**
|
||||
* @param array<string, HtmlSanitizerAction|array<string, bool>> $elementsConfig Registry of allowed/blocked elements:
|
||||
* * If an element is present as a key and contains an array, the element should be allowed
|
||||
* and the array is the list of allowed attributes.
|
||||
* * If an element is present as a key and contains an HtmlSanitizerAction, that action applies.
|
||||
* * If an element is not present as a key, the default action applies.
|
||||
*/
|
||||
public function __construct(
|
||||
private HtmlSanitizerConfig $config,
|
||||
private array $elementsConfig,
|
||||
) {
|
||||
$this->forcedAttributes = $config->getForcedAttributes();
|
||||
|
||||
foreach ($config->getAttributeSanitizers() as $attributeSanitizer) {
|
||||
foreach ($attributeSanitizer->getSupportedElements() ?? ['*'] as $element) {
|
||||
foreach ($attributeSanitizer->getSupportedAttributes() ?? ['*'] as $attribute) {
|
||||
$this->attributeSanitizers[$element][$attribute][] = $attributeSanitizer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->defaultAction = $config->getDefaultAction();
|
||||
}
|
||||
|
||||
public function visit(\Dom\Node|\DOMNode $domNode): ?NodeInterface
|
||||
{
|
||||
$cursor = new Cursor(new DocumentNode());
|
||||
$this->visitChildren($domNode, $cursor);
|
||||
|
||||
return $cursor->node;
|
||||
}
|
||||
|
||||
private function visitNode(\Dom\Node|\DOMNode $domNode, Cursor $cursor): void
|
||||
{
|
||||
$nodeName = StringSanitizer::htmlLower($domNode->nodeName);
|
||||
|
||||
// Visit recursively if the node was not dropped
|
||||
if ($this->enterNode($nodeName, $domNode, $cursor)) {
|
||||
$this->visitChildren($domNode, $cursor);
|
||||
$cursor->node = $cursor->node->getParent();
|
||||
}
|
||||
}
|
||||
|
||||
private function enterNode(string $domNodeName, \Dom\Node|\DOMNode $domNode, Cursor $cursor): bool
|
||||
{
|
||||
if (!\array_key_exists($domNodeName, $this->elementsConfig)) {
|
||||
$action = $this->defaultAction;
|
||||
$allowedAttributes = [];
|
||||
} else {
|
||||
if (\is_array($this->elementsConfig[$domNodeName])) {
|
||||
$action = HtmlSanitizerAction::Allow;
|
||||
$allowedAttributes = $this->elementsConfig[$domNodeName];
|
||||
} else {
|
||||
$action = $this->elementsConfig[$domNodeName];
|
||||
$allowedAttributes = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (HtmlSanitizerAction::Drop === $action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Element should be blocked, retaining its children
|
||||
if (HtmlSanitizerAction::Block === $action) {
|
||||
$node = new BlockedNode($cursor->node);
|
||||
|
||||
$cursor->node->addChild($node);
|
||||
$cursor->node = $node;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise create the node
|
||||
$node = new Node($cursor->node, $domNodeName);
|
||||
$this->setAttributes($domNodeName, $domNode, $node, $allowedAttributes);
|
||||
|
||||
// Force configured attributes
|
||||
foreach ($this->forcedAttributes[$domNodeName] ?? [] as $attribute => $value) {
|
||||
$node->setAttribute($attribute, $value, true);
|
||||
}
|
||||
|
||||
$cursor->node->addChild($node);
|
||||
$cursor->node = $node;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function visitChildren(\Dom\Node|\DOMNode $domNode, Cursor $cursor): void
|
||||
{
|
||||
/** @var \Dom\Node|\DOMNode $child */
|
||||
foreach ($domNode->childNodes ?? [] as $child) {
|
||||
if ('#text' === $child->nodeName) {
|
||||
// Add text directly for performance
|
||||
$cursor->node->addChild(new TextNode($cursor->node, $child instanceof \Dom\Node ? ($child->textContent ?? '') : $child->nodeValue));
|
||||
} elseif (!$child instanceof \Dom\Text && !$child instanceof \Dom\ProcessingInstruction && !$child instanceof \DOMText && !$child instanceof \DOMProcessingInstruction) {
|
||||
// Otherwise continue the visit recursively
|
||||
// Ignore comments for security reasons (interpreted differently by browsers)
|
||||
// Ignore processing instructions (treated as comments)
|
||||
$this->visitNode($child, $cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attributes from a DOM node to a sanitized node.
|
||||
*/
|
||||
private function setAttributes(string $domNodeName, \Dom\Node|\DOMNode $domNode, Node $node, array $allowedAttributes = []): void
|
||||
{
|
||||
/** @var iterable<\Dom\Attr|\DOMAttr> $domAttributes */
|
||||
if (!$domAttributes = $domNode->attributes?->getIterator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($domAttributes as $attribute) {
|
||||
$name = StringSanitizer::htmlLower($attribute->name);
|
||||
|
||||
if (isset($allowedAttributes[$name])) {
|
||||
$value = $attribute->value;
|
||||
|
||||
// Sanitize the attribute value if there are attribute sanitizers for it
|
||||
$attributeSanitizers = array_merge(
|
||||
$this->attributeSanitizers[$domNodeName][$name] ?? [],
|
||||
$this->attributeSanitizers['*'][$name] ?? [],
|
||||
$this->attributeSanitizers[$domNodeName]['*'] ?? [],
|
||||
$this->attributeSanitizers['*']['*'] ?? [],
|
||||
);
|
||||
|
||||
foreach ($attributeSanitizers as $sanitizer) {
|
||||
if (null === $sanitizedValue = $sanitizer->sanitizeAttribute($domNodeName, $name, $value, $this->config)) {
|
||||
continue 2;
|
||||
}
|
||||
$value = $sanitizedValue;
|
||||
}
|
||||
|
||||
$node->setAttribute($name, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
vendor/symfony/html-sanitizer/Visitor/Model/Cursor.php
vendored
Normal file
26
vendor/symfony/html-sanitizer/Visitor/Model/Cursor.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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\HtmlSanitizer\Visitor\Model;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\Visitor\Node\NodeInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Cursor
|
||||
{
|
||||
public function __construct(public ?NodeInterface $node)
|
||||
{
|
||||
}
|
||||
}
|
||||
45
vendor/symfony/html-sanitizer/Visitor/Node/BlockedNode.php
vendored
Normal file
45
vendor/symfony/html-sanitizer/Visitor/Node/BlockedNode.php
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\HtmlSanitizer\Visitor\Node;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
final class BlockedNode implements NodeInterface
|
||||
{
|
||||
private array $children = [];
|
||||
|
||||
public function __construct(
|
||||
private NodeInterface $parentNode,
|
||||
) {
|
||||
}
|
||||
|
||||
public function addChild(NodeInterface $node): void
|
||||
{
|
||||
$this->children[] = $node;
|
||||
}
|
||||
|
||||
public function getParent(): ?NodeInterface
|
||||
{
|
||||
return $this->parentNode;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$rendered = '';
|
||||
foreach ($this->children as $child) {
|
||||
$rendered .= $child->render();
|
||||
}
|
||||
|
||||
return $rendered;
|
||||
}
|
||||
}
|
||||
40
vendor/symfony/html-sanitizer/Visitor/Node/DocumentNode.php
vendored
Normal file
40
vendor/symfony/html-sanitizer/Visitor/Node/DocumentNode.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\HtmlSanitizer\Visitor\Node;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
final class DocumentNode implements NodeInterface
|
||||
{
|
||||
private array $children = [];
|
||||
|
||||
public function addChild(NodeInterface $node): void
|
||||
{
|
||||
$this->children[] = $node;
|
||||
}
|
||||
|
||||
public function getParent(): ?NodeInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$rendered = '';
|
||||
foreach ($this->children as $child) {
|
||||
$rendered .= $child->render();
|
||||
}
|
||||
|
||||
return $rendered;
|
||||
}
|
||||
}
|
||||
121
vendor/symfony/html-sanitizer/Visitor/Node/Node.php
vendored
Normal file
121
vendor/symfony/html-sanitizer/Visitor/Node/Node.php
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
<?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\HtmlSanitizer\Visitor\Node;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\TextSanitizer\StringSanitizer;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
final class Node implements NodeInterface
|
||||
{
|
||||
// HTML5 elements which are self-closing
|
||||
private const VOID_ELEMENTS = [
|
||||
'area' => true,
|
||||
'base' => true,
|
||||
'br' => true,
|
||||
'col' => true,
|
||||
'embed' => true,
|
||||
'hr' => true,
|
||||
'img' => true,
|
||||
'input' => true,
|
||||
'keygen' => true,
|
||||
'link' => true,
|
||||
'meta' => true,
|
||||
'param' => true,
|
||||
'source' => true,
|
||||
'track' => true,
|
||||
'wbr' => true,
|
||||
];
|
||||
|
||||
private array $attributes = [];
|
||||
private array $children = [];
|
||||
|
||||
public function __construct(
|
||||
private NodeInterface $parent,
|
||||
private string $tagName,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getParent(): ?NodeInterface
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function getAttribute(string $name): ?string
|
||||
{
|
||||
return $this->attributes[$name] ?? null;
|
||||
}
|
||||
|
||||
public function setAttribute(string $name, ?string $value, bool $override = false): void
|
||||
{
|
||||
// Always use only the first declaration (ease sanitization)
|
||||
if ($override || !\array_key_exists($name, $this->attributes)) {
|
||||
$this->attributes[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function addChild(NodeInterface $node): void
|
||||
{
|
||||
$this->children[] = $node;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
if (isset(self::VOID_ELEMENTS[$this->tagName])) {
|
||||
return '<'.$this->tagName.$this->renderAttributes().' />';
|
||||
}
|
||||
|
||||
$rendered = '<'.$this->tagName.$this->renderAttributes().'>';
|
||||
foreach ($this->children as $child) {
|
||||
$rendered .= $child->render();
|
||||
}
|
||||
|
||||
return $rendered.'</'.$this->tagName.'>';
|
||||
}
|
||||
|
||||
private function renderAttributes(): string
|
||||
{
|
||||
$rendered = [];
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
if (null === $value) {
|
||||
// Tag should be removed as a sanitizer found suspect data inside
|
||||
continue;
|
||||
}
|
||||
|
||||
$attr = StringSanitizer::encodeHtmlEntities($name);
|
||||
|
||||
if ('' !== $value) {
|
||||
// In quirks mode, IE8 does a poor job producing innerHTML values.
|
||||
// If JavaScript does:
|
||||
// nodeA.innerHTML = nodeB.innerHTML;
|
||||
// and nodeB contains (or even if ` was encoded properly):
|
||||
// <div attr="``foo=bar">
|
||||
// then IE8 will produce:
|
||||
// <div attr=``foo=bar>
|
||||
// as the value of nodeB.innerHTML and assign it to nodeA.
|
||||
// IE8's HTML parser treats `` as a blank attribute value and foo=bar becomes a separate attribute.
|
||||
// Adding a space at the end of the attribute prevents this by forcing IE8 to put double
|
||||
// quotes around the attribute when computing nodeB.innerHTML.
|
||||
if (str_contains($value, '`')) {
|
||||
$value .= ' ';
|
||||
}
|
||||
|
||||
$attr .= '="'.StringSanitizer::encodeHtmlEntities($value).'"';
|
||||
}
|
||||
|
||||
$rendered[] = $attr;
|
||||
}
|
||||
|
||||
return $rendered ? ' '.implode(' ', $rendered) : '';
|
||||
}
|
||||
}
|
||||
37
vendor/symfony/html-sanitizer/Visitor/Node/NodeInterface.php
vendored
Normal file
37
vendor/symfony/html-sanitizer/Visitor/Node/NodeInterface.php
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\HtmlSanitizer\Visitor\Node;
|
||||
|
||||
/**
|
||||
* Represents the sanitized version of a DOM node in the sanitized tree.
|
||||
*
|
||||
* Once the sanitization is done, nodes are rendered into the final output string.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
interface NodeInterface
|
||||
{
|
||||
/**
|
||||
* Add a child node to this node.
|
||||
*/
|
||||
public function addChild(self $node): void;
|
||||
|
||||
/**
|
||||
* Return the parent node of this node, or null if it has no parent node.
|
||||
*/
|
||||
public function getParent(): ?self;
|
||||
|
||||
/**
|
||||
* Render this node as a string, recursively rendering its children as well.
|
||||
*/
|
||||
public function render(): string;
|
||||
}
|
||||
41
vendor/symfony/html-sanitizer/Visitor/Node/TextNode.php
vendored
Normal file
41
vendor/symfony/html-sanitizer/Visitor/Node/TextNode.php
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\HtmlSanitizer\Visitor\Node;
|
||||
|
||||
use Symfony\Component\HtmlSanitizer\TextSanitizer\StringSanitizer;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
final class TextNode implements NodeInterface
|
||||
{
|
||||
public function __construct(
|
||||
private NodeInterface $parentNode,
|
||||
private string $text,
|
||||
) {
|
||||
}
|
||||
|
||||
public function addChild(NodeInterface $node): void
|
||||
{
|
||||
throw new \LogicException('Text nodes cannot have children.');
|
||||
}
|
||||
|
||||
public function getParent(): ?NodeInterface
|
||||
{
|
||||
return $this->parentNode;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return StringSanitizer::encodeHtmlEntities($this->text);
|
||||
}
|
||||
}
|
||||
30
vendor/symfony/html-sanitizer/composer.json
vendored
Normal file
30
vendor/symfony/html-sanitizer/composer.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "symfony/html-sanitizer",
|
||||
"type": "library",
|
||||
"description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.",
|
||||
"keywords": ["html", "sanitizer", "purifier"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Titouan Galopin",
|
||||
"email": "galopintitouan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.4.1",
|
||||
"ext-dom": "*",
|
||||
"league/uri": "^6.5|^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\HtmlSanitizer\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
Reference in New Issue
Block a user