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

137 lines
5.0 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\BlogImport\Model;
use Magento\Framework\Exception\LocalizedException;
class HtmlProcessor
{
private const ESUN_MEDIA_HOSTS = ['esun3d.com', 'www.esun3d.com'];
private const MEDIA_EXTENSIONS = [
'avif', 'bmp', 'gif', 'jpeg', 'jpg', 'm4v', 'mov', 'mp4', 'ogg', 'png', 'svg', 'tif', 'tiff', 'webm', 'webp',
];
/**
* @return array{html: string, media: string[]}
* @throws LocalizedException
*/
public function process(string $htmlPath, string $mediaSubdirectory): array
{
$html = file_get_contents($htmlPath);
if ($html === false || trim($html) === '') {
throw new LocalizedException(__('HTML file is empty or unreadable: %1', $htmlPath));
}
$previous = libxml_use_internal_errors(true);
$document = new \DOMDocument('1.0', 'UTF-8');
$loaded = $document->loadHTML(
'<?xml encoding="UTF-8"><div id="esun-import-root">' . $html . '</div>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if (!$loaded) {
throw new LocalizedException(__('Unable to parse HTML file: %1', $htmlPath));
}
$xpath = new \DOMXPath($document);
$article = $xpath->query(
'//*[contains(concat(" ", normalize-space(@class), " "), " blog-article ")]'
)->item(0);
$contentRoot = $article instanceof \DOMElement
? $article
: $xpath->query('//*[@id="esun-import-root"]')->item(0);
if (!$contentRoot instanceof \DOMElement) {
throw new LocalizedException(__('No importable HTML content found in %1.', $htmlPath));
}
$removeQueries = [
'.//h1[contains(concat(" ", normalize-space(@class), " "), " news-title ")]',
'.//*[contains(concat(" ", normalize-space(@class), " "), " news-time ")]',
'.//script',
'.//*[contains(concat(" ", normalize-space(@class), " "), " picker ")]',
'./*[contains(concat(" ", normalize-space(@class), " "), " clear ")]',
];
foreach ($removeQueries as $query) {
$nodes = iterator_to_array($xpath->query($query, $contentRoot));
foreach ($nodes as $node) {
$node->parentNode?->removeChild($node);
}
}
$media = [];
$mediaReplacements = [];
foreach (iterator_to_array($xpath->query('.//*', $contentRoot)) as $element) {
if (!$element instanceof \DOMElement) {
continue;
}
foreach (['src', 'href', 'poster'] as $attribute) {
if (!$element->hasAttribute($attribute)) {
continue;
}
$relativeMediaPath = $this->getLegacyMediaPath($element->getAttribute($attribute));
if ($relativeMediaPath === null) {
continue;
}
$media[$relativeMediaPath] = $relativeMediaPath;
$placeholder = sprintf('__ESUN_IMPORTED_MEDIA_%d__', count($mediaReplacements));
$mediaReplacements[$placeholder] = sprintf(
"{{media url='%s/%s'}}",
$mediaSubdirectory,
str_replace("'", '&#039;', $relativeMediaPath)
);
$element->setAttribute($attribute, $placeholder);
}
}
$output = '';
foreach ($contentRoot->childNodes as $child) {
$output .= $document->saveHTML($child);
}
$output = trim(strtr($output, $mediaReplacements));
if ($output === '') {
throw new LocalizedException(__('HTML content became empty after processing: %1', $htmlPath));
}
return ['html' => $output, 'media' => array_values($media)];
}
private function getLegacyMediaPath(string $url): ?string
{
$url = html_entity_decode(trim($url), ENT_QUOTES | ENT_HTML5);
$path = '';
if (str_starts_with($url, '/uploads/')) {
$embeddedUrl = rawurldecode(substr($url, strlen('/uploads/')));
if (preg_match('#^(?:https?:)?//#iu', $embeddedUrl)) {
return null;
}
$path = $url;
} elseif (preg_match(
'#^(?:https?:)?//([^/]+)(/.*)$#iu',
$url,
$matches
)) {
if (!in_array(strtolower($matches[1]), self::ESUN_MEDIA_HOSTS, true)) {
return null;
}
$path = $matches[2];
}
if ($path === '') {
return null;
}
$path = (string)preg_split('/[?#]/', $path, 2)[0];
$filename = rawurldecode(basename($path));
if ($filename === '' || $filename === '.' || $filename === '..') {
return null;
}
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($extension, self::MEDIA_EXTENSIONS, true)) {
return null;
}
return $filename;
}
}