Files
esun3d/app/code/Esun/Translation/Model/Html/HtmlTranslationValidator.php
T
2026-09-15 10:35:56 +08:00

110 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Translation\Model\Html;
use DOMDocument;
use DOMElement;
use DOMXPath;
class HtmlTranslationValidator
{
/**
* DOMDocument::loadHTML uses an HTML4 parser and reports safe HTML5/Page Builder tags as invalid.
*/
private const HTML5_ALLOWED_INVALID_TAGS = [
'article',
'aside',
'audio',
'canvas',
'figcaption',
'figure',
'footer',
'header',
'main',
'nav',
'picture',
'section',
'source',
'summary',
'template',
'time',
'video',
];
/**
* @return string[]
*/
public function validate(string $html): array
{
$errors = [];
if (substr_count($html, '{{') !== substr_count($html, '}}')) {
$errors[] = 'Magento directive braces are unbalanced.';
}
$document = new DOMDocument('1.0', 'UTF-8');
$previous = libxml_use_internal_errors(true);
$loaded = $document->loadHTML(
'<?xml encoding="UTF-8"><div>' . $html . '</div>',
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);
}
}