Private
Public Access
173 lines
5.0 KiB
PHP
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;
|
|
}
|
|
}
|