loadHTML(
'
' . $html . '
',
LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED
);
$libxmlErrors = libxml_get_errors();
libxml_clear_errors();
libxml_use_internal_errors($previous);
if (!$loaded) {
return ['HTML cannot be parsed.'];
}
foreach ($libxmlErrors as $error) {
if ($error->level >= LIBXML_ERR_ERROR) {
if ($this->isAllowedHtml5InvalidTagMessage(trim($error->message))) {
continue;
}
$errors[] = trim($error->message);
}
}
$xpath = new DOMXPath($document);
foreach (['script', 'iframe', 'object', 'embed'] as $tag) {
$nodes = $xpath->query('//' . $tag);
if ($nodes !== false && $nodes->length > 0) {
$errors[] = sprintf('Disallowed <%s> tag found.', $tag);
}
}
$allNodes = $xpath->query('//*');
if ($allNodes !== false) {
foreach ($allNodes as $node) {
if (!$node instanceof DOMElement || !$node->hasAttributes()) {
continue;
}
foreach ($node->attributes as $attribute) {
$name = strtolower($attribute->nodeName);
$value = strtolower(trim($attribute->nodeValue ?? ''));
if (str_starts_with($name, 'on')) {
$errors[] = sprintf('Disallowed event attribute "%s" found.', $name);
}
if (in_array($name, ['href', 'src'], true) && str_starts_with($value, 'javascript:')) {
$errors[] = sprintf('Disallowed javascript URL in "%s" found.', $name);
}
}
}
}
return array_values(array_unique(array_filter($errors)));
}
private function isAllowedHtml5InvalidTagMessage(string $message): bool
{
if (!preg_match('/^Tag ([a-z0-9:-]+) invalid$/i', $message, $matches)) {
return false;
}
return in_array(strtolower($matches[1]), self::HTML5_ALLOWED_INVALID_TAGS, true);
}
}