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); } }