]> BookStack Code Mirror - bookstack/blob - app/Util/HtmlDescriptionFilter.php
Comments: Switched to lexical editor
[bookstack] / app / Util / HtmlDescriptionFilter.php
1 <?php
2
3 namespace BookStack\Util;
4
5 use DOMAttr;
6 use DOMElement;
7 use DOMNode;
8
9 /**
10  * Filter to ensure HTML input for description content remains simple and
11  * to a limited allow-list of elements and attributes.
12  * More for consistency and to prevent nuisance rather than for security
13  * (which would be done via a separate content filter and CSP).
14  */
15 class HtmlDescriptionFilter
16 {
17     /**
18      * @var array<string, string[]>
19      */
20     protected static array $allowedAttrsByElements = [
21         'p' => [],
22         'a' => ['href', 'title', 'target'],
23         'ol' => [],
24         'ul' => [],
25         'li' => [],
26         'strong' => [],
27         'span' => [],
28         'em' => [],
29         'br' => [],
30     ];
31
32     public static function filterFromString(string $html): string
33     {
34         if (empty(trim($html))) {
35             return '';
36         }
37
38         $doc = new HtmlDocument($html);
39
40         $topLevel = [...$doc->getBodyChildren()];
41         foreach ($topLevel as $child) {
42             /** @var DOMNode $child */
43             if ($child instanceof DOMElement) {
44                 static::filterElement($child);
45             } else {
46                 $child->parentNode->removeChild($child);
47             }
48         }
49
50         return $doc->getBodyInnerHtml();
51     }
52
53     protected static function filterElement(DOMElement $element): void
54     {
55         $elType = strtolower($element->tagName);
56         $allowedAttrs = static::$allowedAttrsByElements[$elType] ?? null;
57         if (is_null($allowedAttrs)) {
58             $element->remove();
59             return;
60         }
61
62         $attrs = $element->attributes;
63         for ($i = $attrs->length - 1; $i >= 0; $i--) {
64             /** @var DOMAttr $attr */
65             $attr = $attrs->item($i);
66             $name = strtolower($attr->name);
67             if (!in_array($name, $allowedAttrs)) {
68                 $element->removeAttribute($attr->name);
69             }
70         }
71
72         $childNodes = [...$element->childNodes];
73         foreach ($childNodes as $child) {
74             if ($child instanceof DOMElement) {
75                 static::filterElement($child);
76             }
77         }
78     }
79 }