Files
docset-viewer/lib/docset_renderer.php
T

173 lines
5.0 KiB
PHP

<?php
class DocsetRenderer
{
public static function rewriteAssetHtml(string $html, DocsetRepository $repo, string $docsetId, string $baseDir): string
{
$dom = self::loadHtml($html);
if(!$dom)
return $html;
self::sanitizeDocument($dom);
self::rewriteDocumentUrls($dom, $repo, $docsetId, $baseDir, 'asset');
return $dom->saveHTML();
}
public static function renderInlineHtml(string $html, DocsetRepository $repo, string $docsetId, string $baseDir): string
{
$dom = self::loadHtml($html);
if(!$dom)
return '<pre>'.safe($html).'</pre>';
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('<?xml encoding="utf-8" ?>'.$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;
}
}