depage-forms v1.4.1
html forms made easy
Loading...
Searching...
No Matches
HtmlDom.php
Go to the documentation of this file.
1<?php
2
9
10namespace Depage\HtmlForm\Abstracts;
11
18class HtmlDom extends \DOMDocument
19{
23 protected $allowedTags = [
24 "p",
25 "br",
26 "h1",
27 "h2",
28 "ul",
29 "ol",
30 "li",
31
32 "a",
33 "b",
34 "strong",
35 "i",
36 "em",
37 ];
38
42 protected $allowedAttributes = [
43 'class',
44 'href',
45 'target',
46 'alt',
47 'title',
48 'data-dbid',
49 ];
50
59 public function __construct($version = "1.0", $encoding = "")
60 {
61 parent::__construct($version, $encoding);
62 }
63
74 public function loadHTML($html, $options = 0): bool
75 {
76 $tmpDOM = new \DOMDocument();
77
78 $encoding = mb_http_input();
79 if ($encoding == '') {
80 $encoding = "utf-8";
81 }
82
83 // @todo take original content-type if available
84 $success = @$tmpDOM->loadHTML("<meta http-equiv=\"content-type\" content=\"text/html; charset=$encoding\">$html", $options);
85
86 $xpath = new \DOMXPath($tmpDOM);
87 $nodelist = $xpath->query("//body/node()");
88
89 $this->resolveExternals = true;
90 $this->loadXML('<?xml version="1.0" encoding="utf-8"?>
91 <!DOCTYPE html [
92 <!ENTITY nbsp "&#160;">
93 ]>
94 <body></body>');
95 if ($tmpDOM->encoding != '') {
96 $this->encoding = $tmpDOM->encoding;
97 }
98 $rootnode = $this->documentElement;
99
100 foreach ($nodelist as $node) {
101 // copy all nodes inside the body tag to target document
102 $newnode = $this->importNode($node, true);
103 $rootnode->appendChild($newnode);
104 }
105
106 return $success;
107 }
108
119 public function cleanHTML($allowedTags = null, $allowedAttributes = null, $wrapTextNodes = true)
120 {
121 $xpath = new \DOMXPath($this);
122
123 if (is_null($allowedTags)) {
125 }
126 if (is_null($allowedAttributes)) {
128 }
129
130 $tags = [];
131 $classByTag = [];
132
133 foreach ($allowedTags as $t) {
134 preg_match("/([a-zA-Z0-9]*)(\.(.*))?/", $t, $matches);
135
136 $tag = $matches[1] ?? "";
137 $class = $matches[3] ?? "";
138 $tags[$tag] = true;
139 if (!isset($classByTag[$tag])) {
140 $classByTag[$tag] = [];
141 }
142 if (!empty($class)) {
143 $classByTag[$tag][] = $class;
144 } else {
145 $classByTag[$tag][] = "";
146 }
147 }
148
149 // Blacklisted event handler attribute pattern for all common DOM events
150 $eventHandlerPattern = '/^on(abort|activate|afterprint|animationend|animationiteration|animationstart|beforeprint|beforeunload|beforecopy|beforecut|beforeeditfocus|beforepaste|beforescriptactivate|blur|bounce|cancel|change|click|close|command|componentending|componentloaded|contextmenu|copy|cut|dbclick|dblclick|delay|delete|drag|dragstart|drop|error|eval|exception|finish|focus|focusin|focusout|formchange|forminput|full-screen-change|fullscreenchange|fullscreenerror|gesturerotate|gesturestart|gestureswipe|gesturechange|hashchange|help|hoveroff|hoveron|input|invalid|keydown|keypress|keyup|listitemsenter|listitemsexit|load|loadeddata|loadedmetadata|loadend|loadstart|MSGestureChange|MSGestureDoubleTap|MSGestureEnd|MSGestureHold|MSGestureStart|MSGestureTap|MSGotPointerCapture|MSGestureTapAndHold|MSGestureUpdate|MSGotPointerCapture|MSHold|MSInertiaStart|MSPointerCancel|MSPointerDown|MSPointerMove|MSPointerOver|MSPointerOut|MSPointerUp|MSPan|MSPanStarted|MSPanupdate|MSRubberSelect|MSRubberSelectEnd|MSRubberSelectStart|MSScroll|MSScrollUpdate|MSManipulationStateChanged|MSGotPointerrcapture|MSReleasePointerCapture|MSScroll|MSScrollUpdate|MSZoom|MSZoomBegin|MSZoomEnd|open|orientationchange|overflowchanged|pause|paste|pauseanimation|restart|restored|save|scroll|seek|show|showMessage|sleep|split|start|submit|suspend|touchcancel|touchenter|touchleave|touchmove|touchstart|transitionend|unload|unpause|unseek|unshow|vibrate|webkitAnimationEnd|webkitAnimationIteration|webkitAnimationStart|webkitTransitionEnd|volumechange|waiting)|$/i';
151
152 $dangerousSchemes = ['javascript', 'vbscript', 'data', 'file', 'blob', 'about'];
153
154 if ($wrapTextNodes) {
155 $nodelist = $xpath->query("//body/text()");
156
157 for ($i = $nodelist->length - 1; $i >= 0; $i--) {
158 $node = $nodelist->item($i);
159
160 if (!empty(trim($node->textContent))) {
161 // put text nodes into additional p when added directly to body
162 $paragraph = $node->parentNode->insertBefore($this->createElement("p"), $node);
163 $paragraph->appendChild($node);
164 } else {
165 // remove empty text nodes
166 $node->parentNode->removeChild($node);
167 }
168 }
169 }
170
171 $nodelist = $xpath->query("//body//*");
172
173 for ($i = $nodelist->length - 1; $i >= 0; $i--) {
174 $node = $nodelist->item($i);
175
176 if (!isset($tags[$node->nodeName])) {
177 // move child nodes before element itself
178 while ($node->firstChild != null) {
179 if ($node->parentNode->nodeName == "body"
180 && $node->firstChild->nodeType == XML_TEXT_NODE
181 && !empty(trim($node->firstChild->textContent))
182 ) {
183 // put text nodes into additional p when added directly to body
184 $paragraph = $node->parentNode->insertBefore($this->createElement("p"), $node);
185 $paragraph->appendChild($node->firstChild);
186 } else {
187 $node->parentNode->insertBefore($node->firstChild, $node);
188 }
189 }
190
191 // delete empty node
192 $node->parentNode->removeChild($node);
193 } else {
194 // test for allowed attributes
195 for ($j = $node->attributes->length - 1; $j >= 0; $j--) {
196 $attr = $node->attributes->item($j);
197 $attrName = strtolower($attr->name);
198
199 // Remove event handler attributes (on*)
200 if (strpos($attrName, 'on') === 0) {
201 $node->removeAttribute($attr->name);
202 continue;
203 }
204
205 // Remove attributes that are not in allowedAttributes
206 if (!in_array($attrName, $allowedAttributes)) {
207 $node->removeAttribute($attr->name);
208 continue;
209 }
210
211 // Sanitize href/src/action attributes to block dangerous URI schemes
212 if (in_array($attrName, ['href', 'src', 'action', 'formaction'])) {
213 $attrValue = trim($attr->value);
214 foreach ($dangerousSchemes as $dangerous) {
215 $prefix = $dangerous . ':';
216 if (stripos($attrValue, $prefix) === 0) {
217 $node->removeAttribute($attr->name);
218 break;
219 }
220 // Also strip leading whitespace/comments that might be used to bypass
221 if (preg_match('/^\s*(?:\/\*.*\*\/\s*)?' . preg_quote($prefix, '/') . '/i', $attrValue)) {
222 $node->removeAttribute($attr->name);
223 break;
224 }
225 }
226 }
227 }
228
229 // remove invalid classnames
230 if ($node->getAttribute("class") != "") {
231 $attr = implode(" ", array_intersect(
232 explode(" ", $node->getAttribute("class")),
233 $classByTag[$node->nodeName],
234 ));
235 if (empty($attr)) {
236 $node->removeAttribute("class");
237 } else {
238 $node->setAttribute("class", $attr);
239 }
240 }
241 }
242 }
243
244 $nodelist = $xpath->query("//p[. = '' and count(br) = 0]");
245
246 foreach ($nodelist as $node) {
247 $node->appendChild($this->createElement("br"));
248 }
249
250 $nodelist = $xpath->query("//li");
251 $parentNodes = ["ul", "ol", "menu"];
252
253 foreach ($nodelist as $node) {
254 if (!in_array($node->parentNode->nodeName, $parentNodes)) {
255 // find previous XML-element-node
256 $previous = $node->previousSibling;
257 while (!is_null($previous) && $previous->nodeType != XML_ELEMENT_NODE) {
258 $previous = $previous->previousSibling;
259 }
260
261 if (!is_null($previous) && in_array($previous->nodeName, $parentNodes)) {
262 // previous element is a list -> add node to it
263 $listNode->appendChild($node);
264 } else {
265 // create a new ul-list and add element to it
266 $listNode = $node->parentNode->insertBefore($this->createElement("ul"), $node);
267 $listNode->appendChild($node);
268 }
269 }
270 }
271
272 $nodelist = $xpath->query("//b[not(node())] | //i[not(node())] | //strong[not(node())] | //span[not(node())] | //a[not(node())] | //u[not(node())]");
273
274 for ($i = $nodelist->length - 1; $i >= 0; $i--) {
275 $node = $nodelist->item($i);
276
277 $node->parentNode->removeChild($node);
278 }
279
280 $nodes = $this->getBodyNodes();
281 if ($nodes->length == 1) {
282 $node = $nodes->item(0);
283 if ($node->nodeName == "p" && $node->childNodes->length == 1 && $node->childNodes->item(0)->nodeName == "br") {
284 $node->parentNode->removeChild($node);
285 }
286 }
287 }
288
295 public function cutToMaxlength($max)
296 {
297 $charsToRemove = mb_strlen($this->documentElement->textContent) - $max;
298
299 if ($charsToRemove <= 0) {
300 return;
301 }
302
303 $xpath = new \DOMXPath($this);
304 $textNodes = $xpath->query("//text()");
305 $i = $textNodes->length - 1;
306 while ($charsToRemove > 0 && $i >= 0) {
307 $n = $textNodes->item($i);
308 $len = mb_strlen($n->textContent);
309 $parent = $n->parentNode;
310
311 if ($len <= $charsToRemove) {
312 $parent->removeChild($n);
313 $charsToRemove -= $len;
314 } else {
315 $restNode = $n->splitText($len - $charsToRemove);
316 $parent->removeChild($restNode);
317 $charsToRemove = 0;
318 }
319
320 // remove empty nodes
321 if (mb_strlen($parent->textContent) == 0) {
322 $parent->parentNode->removeChild($parent);
323 }
324
325 $i--;
326 }
327 }
328
335 public function __toString()
336 {
337 $html = "";
338 foreach ($this->documentElement->childNodes as $node) {
339 $html .= $this->saveHTML($node) . "\n";
340 }
341
342 return $html;
343 }
344
345 public function __serialize(): array
346 {
347 return [
348 'xml' => $this->saveXML(),
349 ];
350 }
351
352 public function __unserialize(array $data): void
353 {
354 $this->loadXML($data['xml']);
355 }
356
362 public function getBodyNodes()
363 {
364 $xpath = new \DOMXPath($this);
365 $nodelist = $xpath->query("//body/*");
366
367 return $nodelist;
368 }
369}
370
371/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
DOMDocument for html-content.
Definition HtmlDom.php:19
__construct($version="1.0", $encoding="")
htmldom class constructor
Definition HtmlDom.php:59
$allowedAttributes
allowedAttributes
Definition HtmlDom.php:42
cutToMaxlength($max)
cutToMaxlength
Definition HtmlDom.php:295
$allowedTags
Tags that are allowed inside of html.
Definition HtmlDom.php:23
cleanHTML($allowedTags=null, $allowedAttributes=null, $wrapTextNodes=true)
cleans up a htmlDOM
Definition HtmlDom.php:119
getBodyNodes()
gets a nodelist with all nodes inside the body
Definition HtmlDom.php:362
loadHTML($html, $options=0)
loads html from a htmls string
Definition HtmlDom.php:74