]> BookStack Code Mirror - bookstack/blob - app/Util/HtmlContentFilter.php
Merge branch 'master' of https://github.com/theodor-franke/BookStack into theodor...
[bookstack] / app / Util / HtmlContentFilter.php
1 <?php
2
3 namespace BookStack\Util;
4
5 use DOMAttr;
6 use DOMDocument;
7 use DOMNodeList;
8 use DOMXPath;
9
10 class HtmlContentFilter
11 {
12     /**
13      * Remove all the script elements from the given HTML.
14      */
15     public static function removeScripts(string $html): string
16     {
17         if (empty($html)) {
18             return $html;
19         }
20
21         $html = '<body>' . $html . '</body>';
22         libxml_use_internal_errors(true);
23         $doc = new DOMDocument();
24         $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
25         $xPath = new DOMXPath($doc);
26
27         // Remove standard script tags
28         $scriptElems = $xPath->query('//script');
29         static::removeNodes($scriptElems);
30
31         // Remove clickable links to JavaScript URI
32         $badLinks = $xPath->query('//*[' . static::xpathContains('@href', 'javascript:') . ']');
33         static::removeNodes($badLinks);
34
35         // Remove forms with calls to JavaScript URI
36         $badForms = $xPath->query('//*[' . static::xpathContains('@action', 'javascript:') . '] | //*[' . static::xpathContains('@formaction', 'javascript:') . ']');
37         static::removeNodes($badForms);
38
39         // Remove meta tag to prevent external redirects
40         $metaTags = $xPath->query('//meta[' . static::xpathContains('@content', 'url') . ']');
41         static::removeNodes($metaTags);
42
43         // Remove data or JavaScript iFrames
44         $badIframes = $xPath->query('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');
45         static::removeNodes($badIframes);
46
47         // Remove elements with a xlink:href attribute
48         // Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.
49         $xlinkHrefAttributes = $xPath->query('//@*[contains(name(), \'xlink:href\')]');
50         static::removeAttributes($xlinkHrefAttributes);
51
52         // Remove 'on*' attributes
53         $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
54         static::removeAttributes($onAttributes);
55
56         $html = '';
57         $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
58         foreach ($topElems as $child) {
59             $html .= $doc->saveHTML($child);
60         }
61
62         return $html;
63     }
64
65     /**
66      * Create a xpath contains statement with a translation automatically built within
67      * to affectively search in a cases-insensitive manner.
68      */
69     protected static function xpathContains(string $property, string $value): string
70     {
71         $value = strtolower($value);
72         $upperVal = strtoupper($value);
73
74         return 'contains(translate(' . $property . ', \'' . $upperVal . '\', \'' . $value . '\'), \'' . $value . '\')';
75     }
76
77     /**
78      * Remove all the given DOMNodes.
79      */
80     protected static function removeNodes(DOMNodeList $nodes): void
81     {
82         foreach ($nodes as $node) {
83             $node->parentNode->removeChild($node);
84         }
85     }
86
87     /**
88      * Remove all the given attribute nodes.
89      */
90     protected static function removeAttributes(DOMNodeList $attrs): void
91     {
92         /** @var DOMAttr $attr */
93         foreach ($attrs as $attr) {
94             $attrName = $attr->nodeName;
95             $attr->parentNode->removeAttribute($attrName);
96         }
97     }
98 }