saveHTML(); } public static function renderInlineHtml(string $html, DocsetRepository $repo, string $docsetId, string $baseDir): string { $dom = self::loadHtml($html); if(!$dom) return '
'.safe($html).''; self::sanitizeDocument($dom); self::rewriteDocumentUrls($dom, $repo, $docsetId, $baseDir, 'view'); $contentNode = self::firstElementById($dom, 'layout-content') ?: self::firstElementByTag($dom, 'main') ?: self::firstElementByTag($dom, 'article') ?: self::firstElementByTag($dom, 'body') ?: $dom->documentElement; return self::innerHtml($contentNode); } private static function loadHtml(string $html): ?DOMDocument { if(!class_exists('DOMDocument')) return null; $internalErrors = libxml_use_internal_errors(true); $dom = new DOMDocument(); $loaded = $dom->loadHTML(''.$html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return $loaded ? $dom : null; } private static function sanitizeDocument(DOMDocument $dom): void { $blockedTags = array('script', 'iframe', 'frame', 'object', 'embed', 'applet'); foreach($blockedTags as $tag) { $nodes = array(); foreach($dom->getElementsByTagName($tag) as $node) $nodes[] = $node; foreach($nodes as $node) $node->parentNode?->removeChild($node); } foreach($dom->getElementsByTagName('*') as $node) { $remove = array(); foreach($node->attributes ?: array() as $attribute) { $name = strtolower($attribute->name); if(str_starts_with($name, 'on')) $remove[] = $attribute->name; } foreach($remove as $name) $node->removeAttribute($name); } } private static function rewriteDocumentUrls(DOMDocument $dom, DocsetRepository $repo, string $docsetId, string $baseDir, string $defaultMode): void { foreach(array('href', 'src', 'action') as $attribute) { $nodes = array(); foreach($dom->getElementsByTagName('*') as $node) { if($node->hasAttribute($attribute)) $nodes[] = $node; } foreach($nodes as $node) { $value = $node->getAttribute($attribute); $mode = $defaultMode; if($defaultMode === 'view' && $attribute !== 'href') $mode = 'asset'; $rewritten = self::rewriteDocsetUrl($value, $repo, $docsetId, $baseDir, $mode); if($rewritten !== $value) $node->setAttribute($attribute, $rewritten); } } } private static function rewriteDocsetUrl(string $value, DocsetRepository $repo, string $docsetId, string $baseDir, string $mode): string { $value = trim($value); if($value === '' || str_starts_with($value, '#')) return $value; $lower = strtolower($value); foreach(array('javascript:', 'vbscript:') as $prefix) { if(str_starts_with($lower, $prefix)) return '#blocked-unsafe-url'; } foreach(array('http://', 'https://', 'mailto:', 'data:', 'about:') as $prefix) { if(str_starts_with($lower, $prefix)) return $value; } $parts = explode('#', $value, 2); $queryParts = explode('?', $parts[0], 2); $path = $queryParts[0]; $anchor = $parts[1] ?? ''; if($path === '') return $value; if(str_starts_with($path, '/')) $target = ltrim($path, '/'); else $target = trim($baseDir === '.' ? '' : $baseDir, '/').'/'.$path; $normalized = self::normalizeRelativePath($target); $targetMode = $mode; if($mode === 'view') { $extension = strtolower(pathinfo($normalized, PATHINFO_EXTENSION)); $targetMode = in_array($extension, array('html', 'htm', '')) ? 'view' : 'asset'; } $url = $targetMode === 'view' ? URL::Link('view', array('docset' => $docsetId, 'path' => $normalized)) : $repo->assetUrl($docsetId, $normalized); if($anchor !== '') $url .= '#'.rawurlencode($anchor); return $url; } private static function normalizeRelativePath(string $path): string { $segments = array(); foreach(explode('/', str_replace('\\', '/', $path)) as $segment) { if($segment === '' || $segment === '.') continue; if($segment === '..') { array_pop($segments); continue; } $segments[] = $segment; } return implode('/', $segments); } private static function firstElementById(DOMDocument $dom, string $id): ?DOMElement { foreach($dom->getElementsByTagName('*') as $node) { if($node instanceof DOMElement && $node->getAttribute('id') === $id) return $node; } return null; } private static function firstElementByTag(DOMDocument $dom, string $tag): ?DOMElement { $nodes = $dom->getElementsByTagName($tag); $node = $nodes->item(0); return $node instanceof DOMElement ? $node : null; } private static function innerHtml(DOMNode $node): string { $html = ''; foreach($node->childNodes as $child) $html .= $node->ownerDocument->saveHTML($child); return $html; } }