85 خطوط
2.7 KiB
PHP
85 خطوط
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Courses\Application;
|
|
|
|
use DOMDocument;
|
|
use DOMElement;
|
|
use DOMNode;
|
|
use DOMXPath;
|
|
|
|
final class RichTextSanitizer
|
|
{
|
|
private const ALLOWED = ['p', 'h1', 'h2', 'h3', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'a', 'br'];
|
|
|
|
private const DISCARD = ['script', 'style', 'iframe', 'object', 'embed', 'svg', 'math', 'template'];
|
|
|
|
public function sanitize(string $html): string
|
|
{
|
|
$document = new DOMDocument('1.0', 'UTF-8');
|
|
$previous = libxml_use_internal_errors(true);
|
|
$document->loadHTML('<!doctype html><html><head><meta charset="utf-8"></head><body>'.$html.'</body></html>', LIBXML_HTML_NODEFDTD);
|
|
libxml_clear_errors();
|
|
libxml_use_internal_errors($previous);
|
|
$xpath = new DOMXPath($document);
|
|
$nodes = iterator_to_array($xpath->query('//body//*') ?: []);
|
|
|
|
foreach (array_reverse($nodes) as $node) {
|
|
if (! $node instanceof DOMElement || ! $node->parentNode) {
|
|
continue;
|
|
}
|
|
$tag = strtolower($node->tagName);
|
|
if (in_array($tag, self::DISCARD, true)) {
|
|
$node->parentNode->removeChild($node);
|
|
|
|
continue;
|
|
}
|
|
if (! in_array($tag, self::ALLOWED, true)) {
|
|
$this->unwrap($node);
|
|
|
|
continue;
|
|
}
|
|
|
|
$href = $tag === 'a' ? $node->getAttribute('href') : '';
|
|
while ($node->attributes->length > 0) {
|
|
$node->removeAttributeNode($node->attributes->item(0));
|
|
}
|
|
if ($tag === 'a' && $this->safeHref($href)) {
|
|
$node->setAttribute('href', $href);
|
|
$node->setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
}
|
|
|
|
$body = $document->getElementsByTagName('body')->item(0);
|
|
if (! $body) {
|
|
return '';
|
|
}
|
|
|
|
return collect(iterator_to_array($body->childNodes))
|
|
->map(fn (DOMNode $node): string => $document->saveHTML($node) ?: '')
|
|
->implode('');
|
|
}
|
|
|
|
private function unwrap(DOMElement $node): void
|
|
{
|
|
$parent = $node->parentNode;
|
|
while ($node->firstChild) {
|
|
$parent->insertBefore($node->firstChild, $node);
|
|
}
|
|
$parent->removeChild($node);
|
|
}
|
|
|
|
private function safeHref(string $href): bool
|
|
{
|
|
$href = trim($href);
|
|
if ($href === '') {
|
|
return false;
|
|
}
|
|
if (str_starts_with($href, '/') && ! str_starts_with($href, '//')) {
|
|
return true;
|
|
}
|
|
$scheme = strtolower((string) parse_url($href, PHP_URL_SCHEME));
|
|
|
|
return in_array($scheme, ['http', 'https', 'mailto'], true);
|
|
}
|
|
}
|