PluginProbe
PDF for WPForms + Drag and Drop Template Builder / trunk
PDF for WPForms + Drag and Drop Template Builder vtrunk
7.2.0 7.1.0 trunk 2.5.0 3.0.0 6.2.1 6.3.1 6.5.0 6.5.1 6.5.2
pdf-for-wpforms / libs / HtmlDocument.php

HtmlDocument.php in PDF for WPForms + Drag and Drop Template Builder trunk, at libs/HtmlDocument.php

1,107 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace simplehtmldom;
4
5 if (! defined('ABSPATH')) exit; // Exit if accessed directly
6 /**
7 * Website: http://sourceforge.net/projects/simplehtmldom/
8 * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/)
9 *
10 * Licensed under The MIT License
11 * See the LICENSE file in the project root for more information.
12 *
13 * Authors:
14 * S.C. Chen
15 * John Schlick
16 * Rus Carroll
17 * logmanoriginal
18 *
19 * Contributors:
20 * Yousuke Kumakura
21 * Vadim Voituk
22 * Antcs
23 *
24 * Version Rev. 2.0-RC2 (415)
25 */
26
27 include_once 'constants.php';
28 include_once 'HtmlNode.php';
29 include_once 'Debug.php';
30
31 class HtmlDocument
32 {
33 public $root = null;
34 public $nodes = array();
35 public $callback = null;
36 public $lowercase = false;
37 public $original_size;
38 public $size;
39
40 protected $pos;
41 protected $doc;
42 protected $char;
43
44 protected $cursor;
45 protected $parent;
46 protected $noise = array();
47 protected $token_blank = " \t\r\n";
48 protected $token_equal = ' =/>';
49 protected $token_slash = " />\r\n\t";
50 protected $token_attr = ' >';
51
52 public $_charset = '';
53 public $_target_charset = '';
54
55 public $default_br_text = '';
56 public $default_span_text = '';
57
58 protected $self_closing_tags = array(
59 'area' => 1,
60 'base' => 1,
61 'br' => 1,
62 'col' => 1,
63 'embed' => 1,
64 'hr' => 1,
65 'img' => 1,
66 'input' => 1,
67 'link' => 1,
68 'meta' => 1,
69 'param' => 1,
70 'source' => 1,
71 'track' => 1,
72 'wbr' => 1
73 );
74 protected $block_tags = array(
75 'body' => 1,
76 'div' => 1,
77 'form' => 1,
78 'root' => 1,
79 'span' => 1,
80 'table' => 1
81 );
82 protected $optional_closing_tags = array(
83 // Not optional, see
84 // https://www.w3.org/TR/html/textlevel-semantics.html#the-b-element
85 'b' => array('b' => 1),
86 'dd' => array('dd' => 1, 'dt' => 1),
87 // Not optional, see
88 // https://www.w3.org/TR/html/grouping-content.html#the-dl-element
89 'dl' => array('dd' => 1, 'dt' => 1),
90 'dt' => array('dd' => 1, 'dt' => 1),
91 'li' => array('li' => 1),
92 'optgroup' => array('optgroup' => 1, 'option' => 1),
93 'option' => array('optgroup' => 1, 'option' => 1),
94 'p' => array('p' => 1),
95 'rp' => array('rp' => 1, 'rt' => 1),
96 'rt' => array('rp' => 1, 'rt' => 1),
97 'td' => array('td' => 1, 'th' => 1),
98 'th' => array('td' => 1, 'th' => 1),
99 'tr' => array('td' => 1, 'th' => 1, 'tr' => 1),
100 );
101
102 function __call($func, $args)
103 {
104 // Allow users to call methods with lower_case syntax
105 switch ($func) {
106 case 'load_file':
107 $actual_function = 'loadFile';
108 break;
109 case 'clear':
110 return; /* no-op */
111 default:
112 // trigger_error(
113 // 'Call to undefined method ' . __CLASS__ . '::' . $func . '()', //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
114 // E_USER_ERROR
115 // );
116 }
117
118 // phpcs:ignore Generic.Files.LineLength
119 Debug::log(__CLASS__ . '->' . $func . '() has been deprecated and will be removed in the next major version of simplehtmldom. Use ' . __CLASS__ . '->' . $actual_function . '() instead.');
120
121 return call_user_func_array(array($this, $actual_function), $args);
122 }
123
124 function __construct(
125 $str = null,
126 $lowercase = true,
127 $forceTagsClosed = true,
128 $target_charset = DEFAULT_TARGET_CHARSET,
129 $stripRN = true,
130 $defaultBRText = DEFAULT_BR_TEXT,
131 $defaultSpanText = DEFAULT_SPAN_TEXT,
132 $options = 0
133 ) {
134 if ($str) {
135 if (preg_match('/^http:\/\//i', $str) || is_file($str)) {
136 $this->load_file($str);
137 } else {
138 $this->load(
139 $str,
140 $lowercase,
141 $stripRN,
142 $defaultBRText,
143 $defaultSpanText,
144 $options
145 );
146 }
147 } else {
148 $this->prepare($str, $lowercase, $defaultBRText, $defaultSpanText);
149 }
150 // Forcing tags to be closed implies that we don't trust the html, but
151 // it can lead to parsing errors if we SHOULD trust the html.
152 if (!$forceTagsClosed) {
153 $this->optional_closing_array = array();
154 }
155
156 $this->_target_charset = $target_charset;
157 }
158
159 function __debugInfo()
160 {
161 return array(
162 'root' => $this->root,
163 'noise' => empty($this->noise) ? 'none' : $this->noise,
164 'charset' => $this->_charset,
165 'target charset' => $this->_target_charset,
166 'original size' => $this->original_size
167 );
168 }
169
170 function __destruct()
171 {
172 if (isset($this->nodes)) {
173 foreach ($this->nodes as $n) {
174 $n->clear();
175 }
176 }
177 }
178
179 function load(
180 $str,
181 $lowercase = true,
182 $stripRN = true,
183 $defaultBRText = DEFAULT_BR_TEXT,
184 $defaultSpanText = DEFAULT_SPAN_TEXT,
185 $options = 0
186 ) {
187 // prepare
188 $this->prepare($str, $lowercase, $defaultBRText, $defaultSpanText);
189
190 if ($stripRN) {
191 // Temporarily remove any element that shouldn't loose whitespace
192 $this->remove_noise("'<\s*script[^>]*>(.*?)<\s*/\s*script\s*>'is");
193 $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is");
194 $this->remove_noise("'<!--(.*?)-->'is");
195 $this->remove_noise("'<\s*style[^>]*>(.*?)<\s*/\s*style\s*>'is");
196 $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is");
197
198 // Remove whitespace and newlines between tags
199 $this->doc = preg_replace('/\>([\t\s]*[\r\n]^[\t\s]*)\</m', '><', $this->doc);
200
201 // Remove whitespace and newlines in text
202 $this->doc = preg_replace('/([\t\s]*[\r\n]^[\t\s]*)/m', ' ', $this->doc);
203
204 // Restore temporarily removed elements and calculate new size
205 $this->doc = $this->restore_noise($this->doc);
206 $this->size = strlen($this->doc);
207 }
208
209 $this->remove_noise("'(<\?)(.*?)(\?>)'s", true); // server-side script
210 if (count($this->noise)) {
211 // phpcs:ignore Generic.Files.LineLength
212 Debug::log('Support for server-side scripts has been deprecated and will be removed in the next major version of simplehtmldom.');
213 }
214
215 if ($options & HDOM_SMARTY_AS_TEXT) { // Strip Smarty scripts
216 $this->remove_noise("'(\{\w)(.*?)(\})'s", true);
217 // phpcs:ignore Generic.Files.LineLength
218 Debug::log('Support for Smarty scripts has been deprecated and will be removed in the next major version of simplehtmldom.');
219 }
220
221 // parsing
222 $this->parse($stripRN);
223 // end
224 $this->root->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
225 $this->parse_charset();
226 $this->decode();
227 unset($this->doc);
228
229 // make load function chainable
230 return $this;
231 }
232
233 function set_callback($function_name)
234 {
235 $this->callback = $function_name;
236 }
237
238 function remove_callback()
239 {
240 $this->callback = null;
241 }
242
243 function save($filepath = '')
244 {
245 $ret = $this->root->innertext();
246 if ($filepath !== '') {
247 file_put_contents($filepath, $ret, LOCK_EX);
248 }
249 return $ret;
250 }
251
252 function find($selector, $idx = null, $lowercase = false)
253 {
254 return $this->root->find($selector, $idx, $lowercase);
255 }
256
257 function expect($selector, $idx = null, $lowercase = false)
258 {
259 return $this->root->expect($selector, $idx, $lowercase);
260 }
261
262 /** @codeCoverageIgnore */
263 function dump($show_attr = true)
264 {
265 $this->root->dump($show_attr);
266 }
267
268 protected function prepare(
269 $str,
270 $lowercase = true,
271 $defaultBRText = DEFAULT_BR_TEXT,
272 $defaultSpanText = DEFAULT_SPAN_TEXT
273 ) {
274 $this->clear();
275
276 $this->doc = trim((string) $str);
277 $this->size = strlen($this->doc);
278 $this->original_size = $this->size; // original size of the html
279 $this->pos = 0;
280 $this->cursor = 1;
281 $this->noise = array();
282 $this->nodes = array();
283 $this->lowercase = $lowercase;
284 $this->default_br_text = $defaultBRText;
285 $this->default_span_text = $defaultSpanText;
286 $this->root = new HtmlNode($this);
287 $this->root->tag = 'root';
288 $this->root->_[HtmlNode::HDOM_INFO_BEGIN] = -1;
289 $this->root->nodetype = HtmlNode::HDOM_TYPE_ROOT;
290 $this->parent = $this->root;
291 if ($this->size > 0) {
292 $this->char = $this->doc[0];
293 }
294 }
295
296 protected function decode()
297 {
298 foreach ($this->nodes as $node) {
299 if (isset($node->_[HtmlNode::HDOM_INFO_TEXT])) {
300 $node->_[HtmlNode::HDOM_INFO_TEXT] = html_entity_decode(
301 $this->restore_noise($node->_[HtmlNode::HDOM_INFO_TEXT]),
302 ENT_QUOTES | ENT_HTML5,
303 $this->_target_charset
304 );
305 }
306 if (isset($node->_[HtmlNode::HDOM_INFO_INNER])) {
307 $node->_[HtmlNode::HDOM_INFO_INNER] = html_entity_decode(
308 $this->restore_noise($node->_[HtmlNode::HDOM_INFO_INNER]),
309 ENT_QUOTES | ENT_HTML5,
310 $this->_target_charset
311 );
312 }
313 if (isset($node->attr) && is_array($node->attr)) {
314 foreach ($node->attr as $a => $v) {
315 if ($v === true) continue;
316 $node->attr[$a] = html_entity_decode(
317 $v,
318 ENT_QUOTES | ENT_HTML5,
319 $this->_target_charset
320 );
321 }
322 }
323 }
324 }
325
326 protected function parse($trim = false)
327 {
328 while (true) {
329
330 if ($this->char !== '<') {
331 $content = $this->copy_until_char('<');
332
333 if ($content !== '') {
334
335 // Skip whitespace between tags? (</a> <b>)
336 if ($trim && trim($content) === '') {
337 continue;
338 }
339
340 $node = new HtmlNode($this);
341 ++$this->cursor;
342 $node->_[HtmlNode::HDOM_INFO_TEXT] = $content;
343 $this->link_nodes($node, false);
344 }
345 }
346
347 if ($this->read_tag($trim) === false) {
348 break;
349 }
350 }
351 }
352
353 protected function parse_charset()
354 {
355 $charset = null;
356
357 if (function_exists('get_last_retrieve_url_contents_content_type')) {
358 $contentTypeHeader = get_last_retrieve_url_contents_content_type();
359 $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches);
360 if ($success) {
361 $charset = $matches[1];
362 }
363
364 // phpcs:ignore Generic.Files.LineLength
365 Debug::log('Determining charset using get_last_retrieve_url_contents_content_type() ' . ($success ? 'successful' : 'failed'));
366 }
367
368 if (empty($charset)) {
369 // https://www.w3.org/TR/html/document-metadata.html#statedef-http-equiv-content-type
370 $el = $this->root->find('meta[http-equiv=Content-Type]', 0, true);
371
372 if (!empty($el)) {
373 $fullvalue = $el->content;
374
375 if (!empty($fullvalue)) {
376 $success = preg_match(
377 '/charset=(.+)/i',
378 $fullvalue,
379 $matches
380 );
381
382 if ($success) {
383 $charset = $matches[1];
384 }
385 }
386 }
387 }
388
389 if (empty($charset)) {
390 // https://www.w3.org/TR/html/document-metadata.html#character-encoding-declaration
391 if ($meta = $this->root->find('meta[charset]', 0)) {
392 $charset = $meta->charset;
393 }
394 }
395
396 if (empty($charset)) {
397 // Try to guess the charset based on the content
398 // Requires Multibyte String (mbstring) support (optional)
399 if (function_exists('mb_detect_encoding')) {
400 /**
401 * mb_detect_encoding() is not intended to distinguish between
402 * charsets, especially single-byte charsets. Its primary
403 * purpose is to detect which multibyte encoding is in use,
404 * i.e. UTF-8, UTF-16, shift-JIS, etc.
405 *
406 * -- https://bugs.php.net/bug.php?id=38138
407 *
408 * Adding both CP1251/ISO-8859-5 and CP1252/ISO-8859-1 will
409 * always result in CP1251/ISO-8859-5 and vice versa.
410 *
411 * Thus, only detect if it's either UTF-8 or CP1252/ISO-8859-1
412 * to stay compatible.
413 */
414 $encoding = mb_detect_encoding(
415 $this->doc,
416 array('UTF-8', 'CP1252', 'ISO-8859-1')
417 );
418
419 if ($encoding === 'CP1252' || $encoding === 'ISO-8859-1') {
420 // Due to a limitation of mb_detect_encoding
421 // 'CP1251'/'ISO-8859-5' will be detected as
422 // 'CP1252'/'ISO-8859-1'. This will cause iconv to fail, in
423 // which case we can simply assume it is the other charset.
424 if (!@iconv('CP1252', 'UTF-8', $this->doc)) {
425 $encoding = 'CP1251';
426 }
427 }
428
429 if ($encoding !== false) {
430 $charset = $encoding;
431 }
432 }
433 }
434
435 if (empty($charset)) {
436 Debug::log('Unable to determine charset from source document. Assuming UTF-8');
437 $charset = 'UTF-8';
438 }
439
440 // Since CP1252 is a superset, if we get one of it's subsets, we want
441 // it instead.
442 if ((strtolower($charset) == 'iso-8859-1')
443 || (strtolower($charset) == 'latin1')
444 || (strtolower($charset) == 'latin-1')
445 ) {
446 $charset = 'CP1252';
447 }
448
449 return $this->_charset = $charset;
450 }
451
452 protected function read_tag($trim)
453 {
454 if ($this->char !== '<') { // End Of File
455 $this->root->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
456
457 // We might be in a nest of unclosed elements for which the end tags
458 // can be omitted. Close them for faster seek operations.
459 do {
460 if (isset($this->optional_closing_tags[strtolower($this->parent->tag)])) {
461 $this->parent->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
462 }
463 } while ($this->parent = $this->parent->parent);
464
465 return false;
466 }
467
468 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
469
470 if ($trim) { // "< /html>"
471 $this->skip($this->token_blank);
472 }
473
474 // End tag: https://dev.w3.org/html5/pf-summary/syntax.html#end-tags
475 if ($this->char === '/') {
476 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
477
478 $tag = $this->copy_until_char('>');
479 $tag = $trim ? ltrim($tag, $this->token_blank) : $tag;
480
481 // Skip attributes and whitespace in end tags
482 if ($trim && ($pos = strpos($tag, ' ')) !== false) {
483 // phpcs:ignore Generic.Files.LineLength
484 Debug::log_once('Source document contains superfluous whitespace in end tags (</html >).');
485 $tag = substr($tag, 0, $pos);
486 }
487
488 if (strcasecmp($this->parent->tag, $tag)) { // Parent is not start tag
489 $parent_lower = strtolower($this->parent->tag);
490 $tag_lower = strtolower($tag);
491 if (isset($this->optional_closing_tags[$parent_lower]) && isset($this->block_tags[$tag_lower])) {
492 $org_parent = $this->parent;
493
494 // Look for the start tag
495 while (($this->parent->parent) && strtolower($this->parent->tag) !== $tag_lower) {
496 // Close any unclosed element with optional end tags
497 if (isset($this->optional_closing_tags[strtolower($this->parent->tag)]))
498 $this->parent->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
499 $this->parent = $this->parent->parent;
500 }
501
502 // No start tag, close grandparent
503 if (strtolower($this->parent->tag) !== $tag_lower) {
504 $this->parent = $org_parent;
505
506 if ($this->parent->parent) {
507 $this->parent = $this->parent->parent;
508 }
509
510 $this->parent->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
511 return $this->as_text_node($tag);
512 }
513 } elseif (($this->parent->parent) && isset($this->block_tags[$tag_lower])) {
514 // grandparent exists + current is block tag
515 // Parent has no end tag
516 $this->parent->_[HtmlNode::HDOM_INFO_END] = 0;
517 $org_parent = $this->parent;
518
519 // Find start tag
520 while (($this->parent->parent) && strtolower($this->parent->tag) !== $tag_lower) {
521 $this->parent = $this->parent->parent;
522 }
523
524 // No start tag, close parent
525 if (strtolower($this->parent->tag) !== $tag_lower) {
526 $this->parent = $org_parent; // restore origonal parent
527 $this->parent->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
528 return $this->as_text_node($tag);
529 }
530 } elseif (($this->parent->parent) && strtolower($this->parent->parent->tag) === $tag_lower) {
531 // Grandparent exists and current tag closes it
532 $this->parent->_[HtmlNode::HDOM_INFO_END] = 0;
533 $this->parent = $this->parent->parent;
534 } else { // Random tag, add as text node
535 return $this->as_text_node($tag);
536 }
537 }
538
539 // Link with start tag
540 $this->parent->_[HtmlNode::HDOM_INFO_END] = $this->cursor;
541
542 if ($this->parent->parent) {
543 $this->parent = $this->parent->parent;
544 }
545
546 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
547 return true;
548 }
549
550 // Start tag: https://dev.w3.org/html5/pf-summary/syntax.html#start-tags
551 $node = new HtmlNode($this);
552 $node->_[HtmlNode::HDOM_INFO_BEGIN] = $this->cursor++;
553
554 // Tag name
555 $tag = $this->copy_until($this->token_slash);
556
557 if (isset($tag[0]) && $tag[0] === '!') { // Doctype, CData, Comment
558 if (isset($tag[2]) && $tag[1] === '-' && $tag[2] === '-') { // Comment ("<!--")
559
560 // Go back until $tag only contains start of comment "!--".
561 while (strlen($tag) > 3) {
562 $this->char = $this->doc[--$this->pos]; // previous
563 $tag = substr($tag, 0, strlen($tag) - 1);
564 }
565
566 $node->nodetype = HtmlNode::HDOM_TYPE_COMMENT;
567 $node->tag = 'comment';
568
569 $data = '';
570
571 // There is a rare chance of empty comment: "<!---->"
572 // In which case the current char is the first "-" of the end tag
573 // But the comment could also just be a dash: "<!----->"
574 while (true) {
575 // Copy until first char of end tag
576 $data .= $this->copy_until_char('-');
577
578 // Look ahead in the document, maybe we are at the end
579 if (($this->pos + 3) > $this->size) { // End of document
580 Debug::log('Source document ended unexpectedly!');
581 break;
582 } elseif (substr($this->doc, $this->pos, 3) === '-->') { // end
583 $data .= $this->copy_until_char('>');
584 break;
585 }
586
587 $data .= $this->char;
588 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
589 }
590
591 $tag .= $data;
592 $tag = $this->restore_noise($tag);
593
594 // Comment starts after "!--" and ends before "--" (5 chars total)
595 $node->_[HtmlNode::HDOM_INFO_INNER] = substr($tag, 3, strlen($tag) - 5);
596 } elseif (substr($tag, 1, 7) === '[CDATA[') {
597
598 // Go back until $tag only contains start of cdata "![CDATA[".
599 while (strlen($tag) > 8) {
600 $this->char = $this->doc[--$this->pos]; // previous
601 $tag = substr($tag, 0, strlen($tag) - 1);
602 }
603
604 // CDATA can contain HTML stuff, need to find closing tags first
605 $node->nodetype = HtmlNode::HDOM_TYPE_CDATA;
606 $node->tag = 'cdata';
607
608 $data = '';
609
610 // There is a rare chance of empty CDATA: "<[CDATA[]]>"
611 // In which case the current char is the first "[" of the end tag
612 // But the CDATA could also just be a bracket: "<[CDATA[]]]>"
613 while (true) {
614 // Copy until first char of end tag
615 $data .= $this->copy_until_char(']');
616
617 // Look ahead in the document, maybe we are at the end
618 if (($this->pos + 3) > $this->size) { // End of document
619 Debug::log('Source document ended unexpectedly!');
620 break;
621 } elseif (substr($this->doc, $this->pos, 3) === ']]>') { // end
622 $data .= $this->copy_until_char('>');
623 break;
624 }
625
626 $data .= $this->char;
627 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
628 }
629
630 $tag .= $data;
631 $tag = $this->restore_noise($tag);
632
633 // CDATA starts after "![CDATA[" and ends before "]]" (10 chars total)
634 $node->_[HtmlNode::HDOM_INFO_INNER] = substr($tag, 8, strlen($tag) - 10);
635 } else { // Unknown
636 Debug::log('Source document contains unknown declaration: <' . $tag);
637 $node->nodetype = HtmlNode::HDOM_TYPE_UNKNOWN;
638 $node->tag = 'unknown';
639 }
640
641 $node->_[HtmlNode::HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
642
643 if ($this->char === '>') {
644 $node->_[HtmlNode::HDOM_INFO_TEXT] .= '>';
645 }
646
647 $this->link_nodes($node, true);
648 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
649 return true;
650 }
651
652 if (!preg_match('/^\w[\w:-]*$/', $tag)) { // Invalid tag name
653 $node->_[HtmlNode::HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
654
655 if ($this->char === '>') { // End tag
656 $node->_[HtmlNode::HDOM_INFO_TEXT] .= '>';
657 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
658 }
659
660 $this->link_nodes($node, false);
661 Debug::log('Source document contains invalid tag name: ' . $node->_[HtmlNode::HDOM_INFO_TEXT]);
662 return true;
663 }
664
665 // Valid tag name
666 $node->nodetype = HtmlNode::HDOM_TYPE_ELEMENT;
667 $tag_lower = strtolower($tag);
668 $node->tag = ($this->lowercase) ? $tag_lower : $tag;
669
670 if (isset($this->optional_closing_tags[$tag_lower])) { // Optional closing tag
671 while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)])) {
672 // Previous element was the last element of ancestor
673 $this->parent->_[HtmlNode::HDOM_INFO_END] = $node->_[HtmlNode::HDOM_INFO_BEGIN] - 1;
674 $this->parent = $this->parent->parent;
675 }
676 $node->parent = $this->parent;
677 }
678
679 $guard = 0; // prevent infinity loop
680
681 // [0] Space between tag and first attribute
682 $space = array($this->copy_skip($this->token_blank), '', '');
683
684 do { // Parse attributes
685 $name = $this->copy_until($this->token_equal);
686
687 if ($name === '' && $this->char !== null && $space[0] === '') {
688 break;
689 }
690
691 if ($guard === $this->pos) { // Escape infinite loop
692 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
693 continue;
694 }
695
696 $guard = $this->pos;
697
698 if ($this->pos >= $this->size - 1 && $this->char !== '>') { // End Of File
699 Debug::log('Source document ended unexpectedly!');
700 $node->nodetype = HtmlNode::HDOM_TYPE_TEXT;
701 $node->_[HtmlNode::HDOM_INFO_END] = 0;
702 $node->_[HtmlNode::HDOM_INFO_TEXT] = '<' . $tag . $space[0] . $name;
703 $node->tag = 'text';
704 $this->link_nodes($node, false);
705 return true;
706 }
707
708 if ($name === '/' || $name === '') { // No more attributes
709 break;
710 }
711
712 // [1] Whitespace after attribute name
713 $space[1] = (strpos($this->token_blank, $this->char) === false) ? '' : $this->copy_skip($this->token_blank);
714
715 $name = $this->restore_noise($name); // might be a noisy name
716
717 if ($this->lowercase) {
718 $name = strtolower($name);
719 }
720
721 if ($this->char === '=') { // Attribute with value
722 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
723 $this->parse_attr($node, $name, $space, $trim); // get attribute value
724 } else { // Attribute without value
725 $node->_[HtmlNode::HDOM_INFO_QUOTE][$name] = HtmlNode::HDOM_QUOTE_NO;
726 $node->attr[$name] = true;
727 if ($this->char !== '>') {
728 $this->char = $this->doc[--$this->pos];
729 } // prev
730 }
731
732 // Space before attribute and around equal sign
733 if (!$trim && $space !== array(' ', '', '')) {
734 // phpcs:ignore Generic.Files.LineLength
735 Debug::log_once('Source document contains superfluous whitespace in attributes (<e attribute = "value">). Enable trimming or fix attribute spacing for best performance.');
736 $node->_[HtmlNode::HDOM_INFO_SPACE][$name] = $space;
737 }
738
739 // prepare for next attribute
740 $space = array(
741 ((strpos($this->token_blank, $this->char) === false) ? '' : $this->copy_skip($this->token_blank)),
742 '',
743 ''
744 );
745 } while ($this->char !== '>' && $this->char !== '/');
746
747 $this->link_nodes($node, true);
748
749 // Space after last attribute before closing the tag
750 if (!$trim && $space[0] !== '') {
751 // phpcs:ignore Generic.Files.LineLength
752 Debug::log_once('Source document contains superfluous whitespace before the closing braket (<e attribute="value" >). Enable trimming or remove spaces before closing brackets for best performance.');
753 $node->_[HtmlNode::HDOM_INFO_ENDSPACE] = $space[0];
754 }
755
756 $rest = ($this->char === '>') ? '' : $this->copy_until_char('>');
757 $rest = ($trim) ? trim($rest) : $rest; // <html / >
758
759 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
760
761 if (trim($rest) === '/') { // Void element
762 if ($rest !== '') {
763 if (isset($node->_[HtmlNode::HDOM_INFO_ENDSPACE])) {
764 $node->_[HtmlNode::HDOM_INFO_ENDSPACE] .= $rest;
765 } else {
766 $node->_[HtmlNode::HDOM_INFO_ENDSPACE] = $rest;
767 }
768 }
769 $node->_[HtmlNode::HDOM_INFO_END] = 0;
770 } elseif (!isset($this->self_closing_tags[strtolower($node->tag)])) {
771 $innertext = $this->copy_until_char('<');
772 if ($innertext !== '') {
773 $node->_[HtmlNode::HDOM_INFO_INNER] = $innertext;
774 }
775 $this->parent = $node;
776 }
777
778 if ($node->tag === 'br') {
779 $node->_[HtmlNode::HDOM_INFO_INNER] = $this->default_br_text;
780 } elseif ($node->tag === 'script') {
781 $data = '';
782
783 // There is a rare chance of empty script: "<script></script>"
784 // In which case the current char is the start of the end tag
785 // But the script could also just contain tags: "<script><div></script>"
786 while (true) {
787 // Copy until first char of end tag
788 $data .= $this->copy_until_char('<');
789
790 // Look ahead in the document, maybe we are at the end
791 if (($this->pos + 9) > $this->size) { // End of document
792 Debug::log('Source document ended unexpectedly!');
793 break;
794 } elseif (substr($this->doc, $this->pos, 8) === '</script') { // end
795 $this->skip('>'); // don't include the end tag
796 break;
797 }
798
799 // Note: A script tag may contain any other tag except </script>
800 // which needs to be escaped as <\/script>
801
802 $data .= $this->char;
803 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
804 }
805
806 $node = new HtmlNode($this);
807 ++$this->cursor;
808 $node->_[HtmlNode::HDOM_INFO_TEXT] = $data;
809 $this->link_nodes($node, false);
810 }
811
812 return true;
813 }
814
815 protected function parse_attr($node, $name, &$space, $trim)
816 {
817 $is_duplicate = isset($node->attr[$name]);
818
819 if (!$is_duplicate) // Copy whitespace between "=" and value
820 $space[2] = (strpos($this->token_blank, $this->char) === false) ? '' : $this->copy_skip($this->token_blank);
821
822 switch ($this->char) {
823 case '"':
824 $quote_type = HtmlNode::HDOM_QUOTE_DOUBLE;
825 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
826 $value = $this->copy_until_char('"');
827 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
828 break;
829 case '\'':
830 // phpcs:ignore Generic.Files.LineLength
831 Debug::log_once('Source document contains attribute values with single quotes (<e attribute=\'value\'>). Use double quotes for best performance.');
832 $quote_type = HtmlNode::HDOM_QUOTE_SINGLE;
833 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
834 $value = $this->copy_until_char('\'');
835 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
836 break;
837 default:
838 // phpcs:ignore Generic.Files.LineLength
839 Debug::log_once('Source document contains attribute values without quotes (<e attribute=value>). Use double quotes for best performance');
840 $quote_type = HtmlNode::HDOM_QUOTE_NO;
841 $value = $this->copy_until($this->token_attr);
842 }
843
844 $value = $this->restore_noise($value);
845
846 if ($trim) {
847 // Attribute values must not contain control characters other than space
848 // https://www.w3.org/TR/html/dom.html#text-content
849 // https://www.w3.org/TR/html/syntax.html#attribute-values
850 // https://www.w3.org/TR/xml/#AVNormalize
851 $value = preg_replace("/[\r\n\t\s]+/u", ' ', $value);
852 $value = trim($value);
853 }
854
855 if (!$is_duplicate) {
856 if ($quote_type !== HtmlNode::HDOM_QUOTE_DOUBLE) {
857 $node->_[HtmlNode::HDOM_INFO_QUOTE][$name] = $quote_type;
858 }
859 $node->attr[$name] = $value;
860 }
861 }
862
863 protected function link_nodes(&$node, $is_child)
864 {
865 $node->parent = $this->parent;
866 $this->parent->nodes[] = $node;
867 if ($is_child) {
868 $this->parent->children[] = $node;
869 }
870 }
871
872 protected function as_text_node($tag)
873 {
874 $node = new HtmlNode($this);
875 ++$this->cursor;
876 $node->_[HtmlNode::HDOM_INFO_TEXT] = '</' . $tag . '>';
877 $this->link_nodes($node, false);
878 $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
879 return true;
880 }
881
882 protected function skip($chars)
883 {
884 $this->pos += strspn($this->doc, $chars, $this->pos);
885 $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
886 }
887
888 protected function copy_skip($chars)
889 {
890 $pos = $this->pos;
891 $len = strspn($this->doc, $chars, $pos);
892 if ($len === 0) {
893 return '';
894 }
895 $this->pos += $len;
896 $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
897 return substr($this->doc, $pos, $len);
898 }
899
900 protected function copy_until($chars)
901 {
902 $pos = $this->pos;
903 $len = strcspn($this->doc, $chars, $pos);
904 $this->pos += $len;
905 $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
906 return substr($this->doc, $pos, $len);
907 }
908
909 protected function copy_until_char($char)
910 {
911 if ($this->char === null) {
912 return '';
913 }
914
915 if (($pos = strpos($this->doc, $char, $this->pos)) === false) {
916 $ret = substr($this->doc, $this->pos, $this->size - $this->pos);
917 $this->char = null;
918 $this->pos = $this->size;
919 return $ret;
920 }
921
922 if ($pos === $this->pos) {
923 return '';
924 }
925
926 $pos_old = $this->pos;
927 $this->char = $this->doc[$pos];
928 $this->pos = $pos;
929 return substr($this->doc, $pos_old, $pos - $pos_old);
930 }
931
932 protected function remove_noise($pattern, $remove_tag = false)
933 {
934 $count = preg_match_all(
935 $pattern,
936 $this->doc,
937 $matches,
938 PREG_SET_ORDER | PREG_OFFSET_CAPTURE
939 );
940
941 for ($i = $count - 1; $i > -1; --$i) {
942 $key = '___noise___' . sprintf('% 5d', count($this->noise) + 1000);
943
944 $idx = ($remove_tag) ? 0 : 1; // 0 = entire match, 1 = submatch
945 $this->noise[$key] = $matches[$i][$idx][0];
946 $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
947 }
948
949 // reset the length of content
950 $this->size = strlen($this->doc);
951
952 if ($this->size > 0) {
953 $this->char = $this->doc[0];
954 }
955 }
956
957 function restore_noise($text)
958 {
959 if (empty($this->noise)) return $text; // nothing to restore
960 $pos = 0;
961 while (($pos = strpos($text, '___noise___', $pos)) !== false) {
962 // Sometimes there is a broken piece of markup, and we don't GET the
963 // pos+11 etc... token which indicates a problem outside of us...
964
965 // todo: "___noise___1000" (or any number with four or more digits)
966 // in the DOM causes an infinite loop which could be utilized by
967 // malicious software
968 if (strlen($text) > $pos + 15) {
969 $key = '___noise___'
970 . $text[$pos + 11]
971 . $text[$pos + 12]
972 . $text[$pos + 13]
973 . $text[$pos + 14]
974 . $text[$pos + 15];
975
976 if (isset($this->noise[$key])) {
977 $text = substr($text, 0, $pos)
978 . $this->noise[$key]
979 . substr($text, $pos + 16);
980
981 unset($this->noise[$key]);
982 } else {
983 Debug::log_once('Noise restoration failed. DOM has been corrupted!');
984 // do this to prevent an infinite loop.
985 // FIXME: THis causes an infinite loop because the keyword ___NOISE___ is included in the key!
986 $text = substr($text, 0, $pos)
987 . 'UNDEFINED NOISE FOR KEY: '
988 . $key
989 . substr($text, $pos + 16);
990 }
991 } else {
992 // There is no valid key being given back to us... We must get
993 // rid of the ___noise___ or we will have a problem.
994 Debug::log_once('Noise restoration failed. The provided key is incomplete: ' . $text);
995 $text = substr($text, 0, $pos)
996 . 'NO NUMERIC NOISE KEY'
997 . substr($text, $pos + 11);
998 }
999 }
1000 return $text;
1001 }
1002
1003 function search_noise($text)
1004 {
1005 foreach ($this->noise as $noiseElement) {
1006 if (strpos($noiseElement, $text) !== false) {
1007 return $noiseElement;
1008 }
1009 }
1010 }
1011
1012 function __toString()
1013 {
1014 return $this->root->innertext();
1015 }
1016
1017 function __get($name)
1018 {
1019 switch ($name) {
1020 case 'outertext':
1021 return $this->root->innertext();
1022 case 'innertext':
1023 return $this->root->innertext();
1024 case 'plaintext':
1025 return $this->root->text();
1026 case 'charset':
1027 return $this->_charset;
1028 case 'target_charset':
1029 return $this->_target_charset;
1030 }
1031 }
1032
1033 function childNodes($idx = -1)
1034 {
1035 return $this->root->childNodes($idx);
1036 }
1037
1038 function firstChild()
1039 {
1040 return $this->root->firstChild();
1041 }
1042
1043 function lastChild()
1044 {
1045 return $this->root->lastChild();
1046 }
1047
1048 function createElement($name, $value = null)
1049 {
1050 $node = new HtmlNode(null);
1051 $node->nodetype = HtmlNode::HDOM_TYPE_ELEMENT;
1052 $node->_[HtmlNode::HDOM_INFO_BEGIN] = 1;
1053 $node->_[HtmlNode::HDOM_INFO_END] = 1;
1054
1055 if ($value !== null) {
1056 $node->_[HtmlNode::HDOM_INFO_INNER] = $value;
1057 }
1058
1059 $node->tag = $name;
1060
1061 return $node;
1062 }
1063
1064 function createTextNode($value)
1065 {
1066 $node = new HtmlNode($this);
1067 $node->nodetype = HtmlNode::HDOM_TYPE_TEXT;
1068
1069 if ($value !== null) {
1070 $node->_[HtmlNode::HDOM_INFO_TEXT] = $value;
1071 }
1072
1073 return $node;
1074 }
1075
1076 function getElementById($id)
1077 {
1078 return $this->find("#$id", 0);
1079 }
1080
1081 function getElementsById($id, $idx = null)
1082 {
1083 return $this->find("#$id", $idx);
1084 }
1085
1086 function getElementByTagName($name)
1087 {
1088 return $this->find($name, 0);
1089 }
1090
1091 function getElementsByTagName($name, $idx = null)
1092 {
1093 return $this->find($name, $idx);
1094 }
1095
1096 function loadFile($file)
1097 {
1098 $args = func_get_args();
1099
1100 if (($doc = call_user_func_array('file_get_contents', $args)) !== false) {
1101 $this->load($doc, true);
1102 } else {
1103 return false;
1104 }
1105 }
1106 }
1107