80 lines
		
	
	
		
			2.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
			
		
		
	
	
			80 lines
		
	
	
		
			2.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
<?php
 | 
						|
 | 
						|
namespace BookStack\Util;
 | 
						|
 | 
						|
use DOMAttr;
 | 
						|
use DOMElement;
 | 
						|
use DOMNamedNodeMap;
 | 
						|
use DOMNode;
 | 
						|
 | 
						|
/**
 | 
						|
 * Filter to ensure HTML input for description content remains simple and
 | 
						|
 * to a limited allow-list of elements and attributes.
 | 
						|
 * More for consistency and to prevent nuisance rather than for security
 | 
						|
 * (which would be done via a separate content filter and CSP).
 | 
						|
 */
 | 
						|
class HtmlDescriptionFilter
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * @var array<string, string[]>
 | 
						|
     */
 | 
						|
    protected static array $allowedAttrsByElements = [
 | 
						|
        'p' => [],
 | 
						|
        'a' => ['href', 'title', 'target'],
 | 
						|
        'ol' => [],
 | 
						|
        'ul' => [],
 | 
						|
        'li' => [],
 | 
						|
        'strong' => [],
 | 
						|
        'em' => [],
 | 
						|
        'br' => [],
 | 
						|
    ];
 | 
						|
 | 
						|
    public static function filterFromString(string $html): string
 | 
						|
    {
 | 
						|
        if (empty(trim($html))) {
 | 
						|
            return '';
 | 
						|
        }
 | 
						|
 | 
						|
        $doc = new HtmlDocument($html);
 | 
						|
 | 
						|
        $topLevel = [...$doc->getBodyChildren()];
 | 
						|
        foreach ($topLevel as $child) {
 | 
						|
            /** @var DOMNode $child */
 | 
						|
            if ($child instanceof DOMElement) {
 | 
						|
                static::filterElement($child);
 | 
						|
            } else {
 | 
						|
                $child->parentNode->removeChild($child);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $doc->getBodyInnerHtml();
 | 
						|
    }
 | 
						|
 | 
						|
    protected static function filterElement(DOMElement $element): void
 | 
						|
    {
 | 
						|
        $elType = strtolower($element->tagName);
 | 
						|
        $allowedAttrs = static::$allowedAttrsByElements[$elType] ?? null;
 | 
						|
        if (is_null($allowedAttrs)) {
 | 
						|
            $element->remove();
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        /** @var DOMNamedNodeMap $attrs */
 | 
						|
        $attrs = $element->attributes;
 | 
						|
        for ($i = $attrs->length - 1; $i >= 0; $i--) {
 | 
						|
            /** @var DOMAttr $attr */
 | 
						|
            $attr = $attrs->item($i);
 | 
						|
            $name = strtolower($attr->name);
 | 
						|
            if (!in_array($name, $allowedAttrs)) {
 | 
						|
                $element->removeAttribute($attr->name);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($element->childNodes as $child) {
 | 
						|
            if ($child instanceof DOMElement) {
 | 
						|
                static::filterElement($child);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |