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