Simplification
This commit is contained in:
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
class MarkdownService extends Prefab
|
||||
{
|
||||
private const TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'strong', 'em', 'a', 'img', 'hr', 'br'];
|
||||
private const ATTRS = [
|
||||
private const ALLOWED_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'strong', 'em', 'a', 'img', 'hr', 'br'];
|
||||
private const ALLOWED_ATTRIBUTES = [
|
||||
'a' => ['href', 'title', 'rel', 'target'],
|
||||
'img' => ['src', 'alt', 'width', 'height', 'loading', 'decoding'],
|
||||
];
|
||||
@@ -17,31 +17,30 @@ class MarkdownService extends Prefab
|
||||
throw new RuntimeException('Ajoute du contenu avant de publier.');
|
||||
}
|
||||
|
||||
$markdown = $this->neutralizeRawHtml($markdown);
|
||||
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
$html = '<div id="content">' . Markdown::instance()->convert($markdown) . '</div>';
|
||||
$html = '<div id="content">' . Markdown::instance()->convert($this->escapeRawHtml($markdown)) . '</div>';
|
||||
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
$doc->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
|
||||
$root = $doc->getElementById('content');
|
||||
if (!$root) {
|
||||
return '';
|
||||
if (!$root instanceof DOMElement) {
|
||||
throw new RuntimeException('Impossible de générer le contenu HTML.');
|
||||
}
|
||||
|
||||
$this->sanitizeChildren($root, $media);
|
||||
$this->sanitizeNode($root, $media);
|
||||
|
||||
$out = '';
|
||||
$output = '';
|
||||
foreach (iterator_to_array($root->childNodes) as $child) {
|
||||
$out .= $doc->saveHTML($child);
|
||||
$output .= $doc->saveHTML($child);
|
||||
}
|
||||
|
||||
return trim($out);
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
private function neutralizeRawHtml(string $markdown): string
|
||||
private function escapeRawHtml(string $markdown): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'~<!--.*?-->|</?[A-Za-z][A-Za-z0-9:-]*(?:\s[^<>]*)?/?>~s',
|
||||
@@ -50,7 +49,7 @@ class MarkdownService extends Prefab
|
||||
) ?? $markdown;
|
||||
}
|
||||
|
||||
private function sanitizeChildren(DOMNode $parent, Media $media): void
|
||||
private function sanitizeNode(DOMNode $parent, Media $media): void
|
||||
{
|
||||
foreach (iterator_to_array($parent->childNodes) as $child) {
|
||||
if (!$child instanceof DOMElement) {
|
||||
@@ -58,60 +57,72 @@ class MarkdownService extends Prefab
|
||||
}
|
||||
|
||||
$tag = strtolower($child->tagName);
|
||||
if (!in_array($tag, self::TAGS, true)) {
|
||||
if (!in_array($tag, self::ALLOWED_TAGS, true)) {
|
||||
$this->unwrap($child);
|
||||
$this->sanitizeChildren($parent, $media);
|
||||
$this->sanitizeNode($parent, $media);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (iterator_to_array($child->attributes) as $attr) {
|
||||
if (!in_array(strtolower($attr->name), self::ATTRS[$tag] ?? [], true)) {
|
||||
$child->removeAttributeNode($attr);
|
||||
foreach (iterator_to_array($child->attributes) as $attribute) {
|
||||
if (!in_array(strtolower($attribute->name), self::ALLOWED_ATTRIBUTES[$tag] ?? [], true)) {
|
||||
$child->removeAttributeNode($attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if ($tag === 'a') {
|
||||
$href = trim((string) $child->getAttribute('href'));
|
||||
if (!$this->allowedHref($href)) {
|
||||
$this->unwrap($child);
|
||||
$this->sanitizeChildren($parent, $media);
|
||||
continue;
|
||||
}
|
||||
|
||||
$child->setAttribute('href', $href);
|
||||
$child->setAttribute('rel', 'noopener noreferrer');
|
||||
|
||||
if (preg_match('~^https?://~i', $href)) {
|
||||
$child->setAttribute('target', '_blank');
|
||||
} else {
|
||||
$child->removeAttribute('target');
|
||||
}
|
||||
$this->sanitizeLink($child);
|
||||
}
|
||||
|
||||
if ($tag === 'img') {
|
||||
$src = trim((string) $child->getAttribute('src'));
|
||||
if (!str_starts_with($src, 'media:')) {
|
||||
$child->parentNode?->removeChild($child);
|
||||
$this->sanitizeImage($child, $media);
|
||||
if (!$child->parentNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = $media->findByFileName(substr($src, 6));
|
||||
if (!$item) {
|
||||
throw new RuntimeException('Une image utilisée dans le Markdown est introuvable.');
|
||||
}
|
||||
|
||||
$child->setAttribute('src', $item['url']);
|
||||
$child->setAttribute('alt', trim((string) $child->getAttribute('alt')) ?: (string) $item['alt']);
|
||||
$child->setAttribute('width', (string) $item['width']);
|
||||
$child->setAttribute('height', (string) $item['height']);
|
||||
$child->setAttribute('loading', 'lazy');
|
||||
$child->setAttribute('decoding', 'async');
|
||||
}
|
||||
|
||||
$this->sanitizeChildren($child, $media);
|
||||
$this->sanitizeNode($child, $media);
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitizeLink(DOMElement $node): void
|
||||
{
|
||||
$href = trim((string) $node->getAttribute('href'));
|
||||
if (!$this->isAllowedHref($href)) {
|
||||
$this->unwrap($node);
|
||||
return;
|
||||
}
|
||||
|
||||
$node->setAttribute('href', $href);
|
||||
$node->setAttribute('rel', 'noopener noreferrer');
|
||||
if (preg_match('~^https?://~i', $href)) {
|
||||
$node->setAttribute('target', '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
$node->removeAttribute('target');
|
||||
}
|
||||
|
||||
private function sanitizeImage(DOMElement $node, Media $media): void
|
||||
{
|
||||
$src = trim((string) $node->getAttribute('src'));
|
||||
if (!str_starts_with($src, 'media:')) {
|
||||
$node->parentNode?->removeChild($node);
|
||||
return;
|
||||
}
|
||||
|
||||
$item = $media->findByFileName(substr($src, 6));
|
||||
if ($item === null) {
|
||||
throw new RuntimeException('Une image utilisée dans le Markdown est introuvable.');
|
||||
}
|
||||
|
||||
$node->setAttribute('src', $item['url']);
|
||||
$node->setAttribute('alt', trim((string) $node->getAttribute('alt')) ?: (string) $item['alt']);
|
||||
$node->setAttribute('width', (string) $item['width']);
|
||||
$node->setAttribute('height', (string) $item['height']);
|
||||
$node->setAttribute('loading', 'lazy');
|
||||
$node->setAttribute('decoding', 'async');
|
||||
}
|
||||
|
||||
private function unwrap(DOMElement $node): void
|
||||
{
|
||||
$parent = $node->parentNode;
|
||||
@@ -131,16 +142,14 @@ class MarkdownService extends Prefab
|
||||
$parent->removeChild($node);
|
||||
}
|
||||
|
||||
private function allowedHref(string $href): bool
|
||||
private function isAllowedHref(string $href): bool
|
||||
{
|
||||
if ($href === '') {
|
||||
if ($href === '' || str_starts_with($href, '//')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_starts_with($href, '#') || str_starts_with($href, '/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preg_match('~^(?:https?://|mailto:)~i', $href)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user