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 / HtmlNode.php

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

1,530 lines 35.2 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 //phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
7
8 /**
9 * Website: http://sourceforge.net/projects/simplehtmldom/
10 * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/)
11 *
12 * Licensed under The MIT License
13 * See the LICENSE file in the project root for more information.
14 *
15 * Authors:
16 * S.C. Chen
17 * John Schlick
18 * Rus Carroll
19 * logmanoriginal
20 *
21 * Contributors:
22 * Yousuke Kumakura
23 * Vadim Voituk
24 * Antcs
25 *
26 * Version Rev. 2.0-RC2 (415)
27 */
28
29 include_once 'constants.php';
30 include_once 'Debug.php';
31
32 class HtmlNode
33 {
34 const HDOM_TYPE_ELEMENT = 1;
35 const HDOM_TYPE_COMMENT = 2;
36 const HDOM_TYPE_TEXT = 3;
37 const HDOM_TYPE_ROOT = 5;
38 const HDOM_TYPE_UNKNOWN = 6;
39 const HDOM_TYPE_CDATA = 7;
40
41 const HDOM_QUOTE_DOUBLE = 0;
42 const HDOM_QUOTE_SINGLE = 1;
43 const HDOM_QUOTE_NO = 3;
44
45 const HDOM_INFO_BEGIN = 0;
46 const HDOM_INFO_END = 1;
47 const HDOM_INFO_QUOTE = 2;
48 const HDOM_INFO_SPACE = 3;
49 const HDOM_INFO_TEXT = 4;
50 const HDOM_INFO_INNER = 5;
51 const HDOM_INFO_OUTER = 6;
52 const HDOM_INFO_ENDSPACE = 7;
53
54 public $nodetype = self::HDOM_TYPE_TEXT;
55 public $tag = 'text';
56 public $attr = array();
57 public $children = array();
58 public $nodes = array();
59 public $parent = null;
60 public $_ = array();
61 private $dom = null;
62
63 function __call($func, $args)
64 {
65 // Allow users to call methods with lower_case syntax
66 switch ($func) {
67 case 'children':
68 $actual_function = 'childNodes';
69 break;
70 case 'first_child':
71 $actual_function = 'firstChild';
72 break;
73 case 'has_child':
74 $actual_function = 'hasChildNodes';
75 break;
76 case 'last_child':
77 $actual_function = 'lastChild';
78 break;
79 case 'next_sibling':
80 $actual_function = 'nextSibling';
81 break;
82 case 'prev_sibling':
83 $actual_function = 'previousSibling';
84 break;
85 default:
86 // trigger_error(
87 // 'Call to undefined method ' . __CLASS__ . '::' . $func . '()',
88 // E_USER_ERROR
89 // );
90 }
91
92 // phpcs:ignore Generic.Files.LineLength
93 Debug::log(__CLASS__ . '->' . $func . '() has been deprecated and will be removed in the next major version of simplehtmldom. Use ' . __CLASS__ . '->' . $actual_function . '() instead.');
94
95 return call_user_func_array(array($this, $actual_function), $args);
96 }
97
98 function __construct($dom)
99 {
100 if ($dom === null) return $this;
101
102 $this->dom = $dom;
103 $dom->nodes[] = $this;
104 }
105
106 function __debugInfo()
107 {
108 // Translate node type to human-readable form
109 switch ($this->nodetype) {
110 case self::HDOM_TYPE_ELEMENT:
111 $nodetype = "HDOM_TYPE_ELEMENT ($this->nodetype)";
112 break;
113 case self::HDOM_TYPE_COMMENT:
114 $nodetype = "HDOM_TYPE_COMMENT ($this->nodetype)";
115 break;
116 case self::HDOM_TYPE_TEXT:
117 $nodetype = "HDOM_TYPE_TEXT ($this->nodetype)";
118 break;
119 case self::HDOM_TYPE_ROOT:
120 $nodetype = "HDOM_TYPE_ROOT ($this->nodetype)";
121 break;
122 case self::HDOM_TYPE_CDATA:
123 $nodetype = "HDOM_TYPE_CDATA ($this->nodetype)";
124 break;
125 case self::HDOM_TYPE_UNKNOWN:
126 default:
127 $nodetype = "HDOM_TYPE_UNKNOWN ($this->nodetype)";
128 }
129
130 return array(
131 'nodetype' => $nodetype,
132 'tag' => $this->tag,
133 'attributes' => empty($this->attr) ? 'none' : $this->attr,
134 'nodes' => empty($this->nodes) ? 'none' : $this->nodes
135 );
136 }
137
138 function __toString()
139 {
140 return $this->outertext();
141 }
142
143 function clear()
144 {
145 unset($this->dom); // Break link to origin
146 unset($this->parent); // Break link to branch
147 }
148
149 /** @codeCoverageIgnore */
150 function dump($show_attr = true, $depth = 0)
151 {
152 echo str_repeat("\t", $depth) . $this->tag;
153
154 if ($show_attr && count($this->attr) > 0) {
155 echo '(';
156 foreach ($this->attr as $k => $v) {
157 echo "[$k]=>\"$v\", ";
158 }
159 echo ')';
160 }
161
162 echo "\n";
163
164 if ($this->nodes) {
165 foreach ($this->nodes as $node) {
166 $node->dump($show_attr, $depth + 1);
167 }
168 }
169 }
170
171 /** @codeCoverageIgnore */
172 function dump_node($echo = true)
173 {
174 $string = $this->tag;
175
176 if (count($this->attr) > 0) {
177 $string .= '(';
178 foreach ($this->attr as $k => $v) {
179 $string .= "[$k]=>\"$v\", ";
180 }
181 $string .= ')';
182 }
183
184 if (count($this->_) > 0) {
185 $string .= ' $_ (';
186 foreach ($this->_ as $k => $v) {
187 if (is_array($v)) {
188 $string .= "[$k]=>(";
189 foreach ($v as $k2 => $v2) {
190 $string .= "[$k2]=>\"$v2\", ";
191 }
192 $string .= ')';
193 } else {
194 $string .= "[$k]=>\"$v\", ";
195 }
196 }
197 $string .= ')';
198 }
199
200 if (isset($this->text)) {
201 $string .= " text: ({$this->text})";
202 }
203
204 $string .= ' HDOM_INNER_INFO: ';
205
206 if (isset($node->_[self::HDOM_INFO_INNER])) {
207 $string .= "'" . $node->_[self::HDOM_INFO_INNER] . "'";
208 } else {
209 $string .= ' NULL ';
210 }
211
212 $string .= ' children: ' . count($this->children);
213 $string .= ' nodes: ' . count($this->nodes);
214 $string .= "\n";
215
216 if ($echo) {
217 echo $string;
218 return;
219 } else {
220 return $string;
221 }
222 }
223
224 function parent($parent = null)
225 {
226 // I am SURE that this doesn't work properly.
227 // It fails to unset the current node from it's current parents nodes or
228 // children list first.
229 if ($parent !== null) {
230 $this->parent = $parent;
231 $this->parent->nodes[] = $this;
232 $this->parent->children[] = $this;
233 }
234
235 return $this->parent;
236 }
237
238 function find_ancestor_tag($tag)
239 {
240 if ($this->parent === null) return null;
241
242 $ancestor = $this->parent;
243
244 while (!is_null($ancestor)) {
245 if ($ancestor->tag === $tag) {
246 break;
247 }
248
249 $ancestor = $ancestor->parent;
250 }
251
252 return $ancestor;
253 }
254
255 function innertext()
256 {
257 if (isset($this->_[self::HDOM_INFO_INNER])) {
258 $ret = $this->_[self::HDOM_INFO_INNER];
259 } elseif (isset($this->_[self::HDOM_INFO_TEXT])) {
260 $ret = $this->_[self::HDOM_INFO_TEXT];
261 } else {
262 $ret = '';
263 }
264
265 foreach ($this->nodes as $n) {
266 $ret .= $n->outertext();
267 }
268
269 return $this->convert_text($ret);
270 }
271
272 function outertext()
273 {
274 if ($this->tag === 'root') {
275 return $this->innertext();
276 }
277
278 // todo: What is the use of this callback? Remove?
279 if ($this->dom && $this->dom->callback !== null) {
280 call_user_func_array($this->dom->callback, array($this));
281 }
282
283 if (isset($this->_[self::HDOM_INFO_OUTER])) {
284 return $this->convert_text($this->_[self::HDOM_INFO_OUTER]);
285 }
286
287 if (isset($this->_[self::HDOM_INFO_TEXT])) {
288 return $this->convert_text($this->_[self::HDOM_INFO_TEXT]);
289 }
290
291 $ret = '';
292
293 if (isset($this->_[self::HDOM_INFO_BEGIN])) {
294 $ret = $this->makeup();
295 }
296
297 if (isset($this->_[self::HDOM_INFO_INNER])) {
298 // todo: <br> should either never have self::HDOM_INFO_INNER or always
299 if ($this->tag !== 'br') {
300 $ret .= $this->_[self::HDOM_INFO_INNER];
301 }
302 }
303
304 if ($this->nodes) {
305 foreach ($this->nodes as $n) {
306 $ret .= $n->outertext();
307 }
308 }
309
310 if (isset($this->_[self::HDOM_INFO_END]) && $this->_[self::HDOM_INFO_END] != 0) {
311 $ret .= '</' . $this->tag . '>';
312 }
313
314 return $this->convert_text($ret);
315 }
316
317 /**
318 * Returns true if the provided element is a block level element
319 * @link https://www.w3resource.com/html/HTML-block-level-and-inline-elements.php
320 */
321 protected function is_block_element($node)
322 {
323 // todo: When we have the utility class this should be moved there
324 return in_array(strtolower($node->tag), array(
325 'p',
326 'h1',
327 'h2',
328 'h3',
329 'h4',
330 'h5',
331 'h6',
332 'ol',
333 'ul',
334 'pre',
335 'address',
336 'blockquote',
337 'dl',
338 'div',
339 'fieldset',
340 'form',
341 'hr',
342 'noscript',
343 'table'
344 ));
345 }
346
347 /**
348 * Returns true if the provided element is an inline level element
349 * @link https://www.w3resource.com/html/HTML-block-level-and-inline-elements.php
350 */
351 protected function is_inline_element($node)
352 {
353 // todo: When we have the utility class this should be moved there
354 return in_array(strtolower($node->tag), array(
355 'b',
356 'big',
357 'i',
358 'small',
359 'tt',
360 'abbr',
361 'acronym',
362 'cite',
363 'code',
364 'dfn',
365 'em',
366 'kbd',
367 'strong',
368 'samp',
369 'var',
370 'a',
371 'bdo',
372 'br',
373 'img',
374 'map',
375 'object',
376 'q',
377 'script',
378 'span',
379 'sub',
380 'sup',
381 'button',
382 'input',
383 'label',
384 'select',
385 'textarea'
386 ));
387 }
388
389 function text($trim = true)
390 {
391 $ret = '';
392
393 if (strtolower($this->tag) === 'script') {
394 $ret = '';
395 } elseif (strtolower($this->tag) === 'style') {
396 $ret = '';
397 } elseif ($this->nodetype === self::HDOM_TYPE_COMMENT) {
398 $ret = '';
399 } elseif ($this->nodetype === self::HDOM_TYPE_CDATA) {
400 $ret = $this->_[self::HDOM_INFO_INNER];
401 } elseif ($this->nodetype === self::HDOM_TYPE_UNKNOWN) {
402 $ret = '';
403 } elseif (isset($this->_[self::HDOM_INFO_INNER])) {
404 $ret = $this->_[self::HDOM_INFO_INNER];
405 } elseif ($this->nodetype === self::HDOM_TYPE_TEXT) {
406 $ret = $this->_[self::HDOM_INFO_TEXT];
407 }
408
409 if (is_null($this->nodes)) {
410 return '';
411 }
412
413 foreach ($this->nodes as $n) {
414 if ($this->is_block_element($n)) {
415
416 $block = ltrim($this->convert_text($n->text(false)));
417
418 if (empty($block))
419 continue;
420
421 $ret = rtrim($ret) . "\n\n" . $block;
422 } elseif ($this->is_inline_element($n)) {
423 // todo: <br> introduces code smell because no space but \n
424 if (strtolower($n->tag) === 'br') {
425 $ret .= $this->dom->default_br_text ?: DEFAULT_BR_TEXT;
426 } else {
427 $inline = ltrim($this->convert_text($n->text(false)));
428
429 if (empty($inline))
430 continue;
431
432 $ret = $ret . $this->convert_text($n->text(false));
433 }
434 } else {
435 $ret .= $this->convert_text($n->text(false));
436 }
437 }
438
439 // Reduce whitespace at start/end to a single (or none) space
440 $ret = preg_replace('/[ \t\n\r\0\x0B\xC2\xA0]+$/u', $trim ? '' : ' ', $ret);
441 $ret = preg_replace('/^[ \t\n\r\0\x0B\xC2\xA0]+/u', $trim ? '' : ' ', $ret);
442
443 return $ret;
444 }
445
446 function xmltext()
447 {
448 $ret = $this->innertext();
449 $ret = str_ireplace('<![CDATA[', '', $ret);
450 $ret = str_replace(']]>', '', $ret);
451 return $ret;
452 }
453
454 function makeup()
455 {
456 // text, comment, unknown
457 if (isset($this->_[self::HDOM_INFO_TEXT])) {
458 return $this->_[self::HDOM_INFO_TEXT];
459 }
460
461 $ret = '<' . $this->tag;
462
463 foreach ($this->attr as $key => $val) {
464
465 // skip removed attribute
466 if ($val === null || $val === false) {
467 continue;
468 }
469
470 if (isset($this->_[self::HDOM_INFO_SPACE][$key])) {
471 $ret .= $this->_[self::HDOM_INFO_SPACE][$key][0];
472 } else {
473 $ret .= ' ';
474 }
475
476 //no value attr: nowrap, checked selected...
477 if ($val === true) {
478 $ret .= $key;
479 } else {
480 if (isset($this->_[self::HDOM_INFO_QUOTE][$key])) {
481 $quote_type = $this->_[self::HDOM_INFO_QUOTE][$key];
482 } else {
483 $quote_type = self::HDOM_QUOTE_DOUBLE;
484 }
485
486 switch ($quote_type) {
487 case self::HDOM_QUOTE_SINGLE:
488 $quote = '\'';
489 $val = htmlentities($val, ENT_QUOTES, $this->dom->target_charset);
490 break;
491 case self::HDOM_QUOTE_NO:
492 $quote = '';
493 break;
494 case self::HDOM_QUOTE_DOUBLE:
495 default:
496 $quote = '"';
497 $val = htmlentities($val, ENT_COMPAT, $this->dom->target_charset);
498 }
499
500 $ret .= $key
501 . (isset($this->_[self::HDOM_INFO_SPACE][$key]) ? $this->_[self::HDOM_INFO_SPACE][$key][1] : '')
502 . '='
503 . (isset($this->_[self::HDOM_INFO_SPACE][$key]) ? $this->_[self::HDOM_INFO_SPACE][$key][2] : '')
504 . $quote
505 . $val
506 . $quote;
507 }
508 }
509
510 if (isset($this->_[self::HDOM_INFO_ENDSPACE])) {
511 $ret .= $this->_[self::HDOM_INFO_ENDSPACE];
512 }
513
514 return $ret . '>';
515 }
516
517 function find($selector, $idx = null, $lowercase = false)
518 {
519 $selectors = $this->parse_selector($selector);
520 if (($count = count($selectors)) === 0) {
521 return array();
522 }
523 $found_keys = array();
524
525 // find each selector
526 for ($c = 0; $c < $count; ++$c) {
527 // The change on the below line was documented on the sourceforge
528 // code tracker id 2788009
529 // used to be: if (($levle=count($selectors[0]))===0) return array();
530 if (($levle = count($selectors[$c])) === 0) {
531 Debug::log_once('Empty selector (' . $selector . ') matches nothing.');
532 return array();
533 }
534
535 if (!isset($this->_[self::HDOM_INFO_BEGIN])) {
536 Debug::log_once('Invalid operation. The current node has no start tag.');
537 return array();
538 }
539
540 $head = array($this->_[self::HDOM_INFO_BEGIN] => 1);
541 $cmd = ' '; // Combinator
542
543 // handle descendant selectors, no recursive!
544 for ($l = 0; $l < $levle; ++$l) {
545 $ret = array();
546
547 foreach ($head as $k => $v) {
548 $n = ($k === -1) ? $this->dom->root : $this->dom->nodes[$k];
549 //PaperG - Pass this optional parameter on to the seek function.
550 $n->seek($selectors[$c][$l], $ret, $cmd, $lowercase);
551 }
552
553 $head = $ret;
554 $cmd = $selectors[$c][$l][6]; // Next Combinator
555 }
556
557 foreach ($head as $k => $v) {
558 if (!isset($found_keys[$k])) {
559 $found_keys[$k] = 1;
560 }
561 }
562 }
563
564 // sort keys
565 ksort($found_keys);
566
567 $found = array();
568 foreach ($found_keys as $k => $v) {
569 $found[] = $this->dom->nodes[$k];
570 }
571
572 // return nth-element or array
573 if (is_null($idx)) {
574 return $found;
575 } elseif ($idx < 0) {
576 $idx = count($found) + $idx;
577 }
578 return (isset($found[$idx])) ? $found[$idx] : null;
579 }
580
581 function expect($selector, $idx = null, $lowercase = false)
582 {
583 return $this->find($selector, $idx, $lowercase) ?: null;
584 }
585
586 protected function seek($selector, &$ret, $parent_cmd, $lowercase = false)
587 {
588 list($ps_selector, $tag, $ps_element, $id, $class, $attributes, $cmb) = $selector;
589 $nodes = array();
590
591 if ($parent_cmd === ' ') { // Descendant Combinator
592 // Find parent closing tag if the current element doesn't have a closing
593 // tag (i.e. void element)
594 $end = (!empty($this->_[self::HDOM_INFO_END])) ? $this->_[self::HDOM_INFO_END] : 0;
595 if ($end == 0 && $this->parent) {
596 $parent = $this->parent;
597 while ($parent !== null && !isset($parent->_[self::HDOM_INFO_END])) {
598 $end -= 1;
599 $parent = $parent->parent;
600 }
601 $end += $parent->_[self::HDOM_INFO_END];
602 }
603
604 if ($end === 0) {
605 $end = count($this->dom->nodes);
606 }
607
608 // Get list of target nodes
609 $nodes_start = $this->_[self::HDOM_INFO_BEGIN] + 1;
610
611 // remove() makes $this->dom->nodes non-contiguous; use what is left.
612 $nodes = array_intersect_key(
613 $this->dom->nodes,
614 array_flip(range($nodes_start, $end))
615 );
616 } elseif ($parent_cmd === '>') { // Child Combinator
617 $nodes = $this->children;
618 } elseif (
619 $parent_cmd === '+'
620 && $this->parent
621 && in_array($this, $this->parent->children)
622 ) { // Next-Sibling Combinator
623 $index = array_search($this, $this->parent->children, true) + 1;
624 if ($index < count($this->parent->children))
625 $nodes[] = $this->parent->children[$index];
626 } elseif (
627 $parent_cmd === '~'
628 && $this->parent
629 && in_array($this, $this->parent->children)
630 ) { // Subsequent Sibling Combinator
631 $index = array_search($this, $this->parent->children, true);
632 $nodes = array_slice($this->parent->children, $index);
633 }
634
635 // Go throgh each element starting at this element until the end tag
636 // Note: If this element is a void tag, any previous void element is
637 // skipped.
638 foreach ($nodes as $node) {
639 $pass = true;
640
641 // Skip root nodes
642 if (!$node->parent) {
643 unset($node);
644 continue;
645 }
646
647 // Handle 'text' selector
648 if ($pass && $tag === 'text') {
649
650 if ($node->tag === 'text') {
651 $ret[array_search($node, $this->dom->nodes, true)] = 1;
652 }
653
654 if (isset($node->_[self::HDOM_INFO_INNER])) {
655 $ret[$node->_[self::HDOM_INFO_BEGIN]] = 1;
656 }
657
658 unset($node);
659 continue;
660 }
661
662 // Handle 'cdata' selector
663 if ($pass && $tag === 'cdata') {
664
665 if ($node->tag === 'cdata') {
666 $ret[$node->_[self::HDOM_INFO_BEGIN]] = 1;
667 }
668
669 unset($node);
670 continue;
671 }
672
673 // Handle 'comment'
674 if ($pass && $tag === 'comment' && $node->tag === 'comment') {
675 $ret[$node->_[self::HDOM_INFO_BEGIN]] = 1;
676 unset($node);
677 continue;
678 }
679
680 // Skip if node isn't a child node (i.e. text nodes)
681 if ($pass && !in_array($node, $node->parent->children, true)) {
682 unset($node);
683 continue;
684 }
685
686 // Skip if tag doesn't match
687 if ($pass && $tag !== '' && $tag !== $node->tag && $tag !== '*') {
688 $pass = false;
689 }
690
691 // Skip if ID doesn't exist
692 if ($pass && $id !== '' && !isset($node->attr['id'])) {
693 $pass = false;
694 }
695
696 // Check if ID matches
697 if ($pass && $id !== '' && isset($node->attr['id'])) {
698 // Note: Only consider the first ID (as browsers do)
699 $node_id = explode(' ', trim($node->attr['id']))[0];
700
701 if ($id !== $node_id) {
702 $pass = false;
703 }
704 }
705
706 // Check if all class(es) exist
707 if ($pass && $class !== '' && is_array($class) && !empty($class)) {
708 if (isset($node->attr['class'])) {
709 // Apply the same rules for the pattern and attribute value
710 // Attribute values must not contain control characters other than space
711 // https://www.w3.org/TR/html/dom.html#text-content
712 // https://www.w3.org/TR/html/syntax.html#attribute-values
713 // https://www.w3.org/TR/xml/#AVNormalize
714 $node_classes = preg_replace("/[\r\n\t\s]+/u", ' ', $node->attr['class']);
715 $node_classes = trim($node_classes);
716 $node_classes = explode(' ', $node_classes);
717
718 if ($lowercase) {
719 $node_classes = array_map('strtolower', $node_classes);
720 }
721
722 foreach ($class as $c) {
723 if (!in_array($c, $node_classes)) {
724 $pass = false;
725 break;
726 }
727 }
728 } else {
729 $pass = false;
730 }
731 }
732
733 // Check attributes
734 if (
735 $pass
736 && $attributes !== ''
737 && is_array($attributes)
738 && !empty($attributes)
739 ) {
740 foreach ($attributes as $a) {
741 list(
742 $att_name,
743 $att_expr,
744 $att_val,
745 $att_inv,
746 $att_case_sensitivity
747 ) = $a;
748
749 // Handle indexing attributes (i.e. "[2]")
750 /**
751 * Note: This is not supported by the CSS Standard but adds
752 * the ability to select items compatible to XPath (i.e.
753 * the 3rd element within it's parent).
754 *
755 * Note: This doesn't conflict with the CSS Standard which
756 * doesn't work on numeric attributes anyway.
757 */
758 if (
759 is_numeric($att_name)
760 && $att_expr === ''
761 && $att_val === ''
762 ) {
763 $count = 0;
764
765 // Find index of current element in parent
766 foreach ($node->parent->children as $c) {
767 if ($c->tag === $node->tag) ++$count;
768 if ($c === $node) break;
769 }
770
771 // If this is the correct node, continue with next
772 // attribute
773 if ($count === (int)$att_name) continue;
774 }
775
776 // Check attribute availability
777 if ($att_inv) { // Attribute should NOT be set
778 if (isset($node->attr[$att_name])) {
779 $pass = false;
780 break;
781 }
782 } else { // Attribute should be set
783 // todo: "plaintext" is not a valid CSS selector!
784 if (
785 $att_name !== 'plaintext'
786 && !isset($node->attr[$att_name])
787 ) {
788 $pass = false;
789 break;
790 }
791 }
792
793 // Continue with next attribute if expression isn't defined
794 if ($att_expr === '') continue;
795
796 // If they have told us that this is a "plaintext"
797 // search then we want the plaintext of the node - right?
798 // todo "plaintext" is not a valid CSS selector!
799 if ($att_name === 'plaintext') {
800 $nodeKeyValue = $node->text();
801 } else {
802 $nodeKeyValue = $node->attr[$att_name];
803 }
804
805 // If lowercase is set, do a case insensitive test of
806 // the value of the selector.
807 if ($lowercase) {
808 $check = $this->match(
809 $att_expr,
810 strtolower($att_val),
811 strtolower($nodeKeyValue),
812 $att_case_sensitivity
813 );
814 } else {
815 $check = $this->match(
816 $att_expr,
817 $att_val,
818 $nodeKeyValue,
819 $att_case_sensitivity
820 );
821 }
822
823 $check = $ps_element === 'not' ? !$check : $check;
824
825 if (!$check) {
826 $pass = false;
827 break;
828 }
829 }
830 }
831
832 // Found a match. Add to list and clear node
833 $pass = $ps_selector === 'not' ? !$pass : $pass;
834 if ($pass) $ret[$node->_[self::HDOM_INFO_BEGIN]] = 1;
835 unset($node);
836 }
837 }
838
839 protected function match($exp, $pattern, $value, $case_sensitivity)
840 {
841 if ($case_sensitivity === 'i') {
842 $pattern = strtolower($pattern);
843 $value = strtolower($value);
844 }
845
846 // Apply the same rules for the pattern and attribute value
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 $pattern = preg_replace("/[\r\n\t\s]+/u", ' ', $pattern);
852 $pattern = trim($pattern);
853
854 $value = preg_replace("/[\r\n\t\s]+/u", ' ', $value);
855 $value = trim($value);
856
857 switch ($exp) {
858 case '=':
859 return ($value === $pattern);
860 case '!=':
861 return ($value !== $pattern);
862 case '^=':
863 return preg_match('/^' . preg_quote($pattern, '/') . '/', $value);
864 case '$=':
865 return preg_match('/' . preg_quote($pattern, '/') . '$/', $value);
866 case '*=':
867 return preg_match('/' . preg_quote($pattern, '/') . '/', $value);
868 case '|=':
869 /**
870 * [att|=val]
871 *
872 * Represents an element with the att attribute, its value
873 * either being exactly "val" or beginning with "val"
874 * immediately followed by "-" (U+002D).
875 */
876 return strpos($value, $pattern) === 0;
877 case '~=':
878 /**
879 * [att~=val]
880 *
881 * Represents an element with the att attribute whose value is a
882 * whitespace-separated list of words, one of which is exactly
883 * "val". If "val" contains whitespace, it will never represent
884 * anything (since the words are separated by spaces). Also if
885 * "val" is the empty string, it will never represent anything.
886 */
887 return in_array($pattern, explode(' ', trim($value)), true);
888 }
889
890 Debug::log('Unhandled attribute selector: ' . $exp . '!');
891 return false;
892 }
893
894 protected function parse_selector($selector_string)
895 {
896 /**
897 * Pattern of CSS selectors, modified from mootools (https://mootools.net/)
898 *
899 * Paperg: Add the colon to the attribute, so that it properly finds
900 * <tag attr:ibute="something" > like google does.
901 *
902 * Note: if you try to look at this attribute, you MUST use getAttribute
903 * since $dom->x:y will fail the php syntax check.
904 *
905 * Notice the \[ starting the attribute? and the @? following? This
906 * implies that an attribute can begin with an @ sign that is not
907 * captured. This implies that an html attribute specifier may start
908 * with an @ sign that is NOT captured by the expression. Farther study
909 * is required to determine of this should be documented or removed.
910 *
911 * Matches selectors in this order:
912 *
913 * [0] - full match
914 *
915 * [1] - pseudo selector
916 * (?:\:(\w+)\()?
917 * Matches the pseudo selector (optional)
918 *
919 * [2] - tag name
920 * ([\w:\*-]*)
921 * Matches the tag name consisting of zero or more words, colons,
922 * asterisks and hyphens.
923 *
924 * [3] - pseudo selector
925 * (?:\:(\w+)\()?
926 * Matches the pseudo selector (optional)
927 *
928 * [4] - id name
929 * (?:\#([\w-]+))
930 * Optionally matches a id name, consisting of an "#" followed by
931 * the id name (one or more words and hyphens).
932 *
933 * [5] - class names (including dots)
934 * (?:\.([\w\.-]+))?
935 * Optionally matches a list of classs, consisting of an "."
936 * followed by the class name (one or more words and hyphens)
937 * where multiple classes can be chained (i.e. ".foo.bar.baz")
938 *
939 * [6] - attributes
940 * ((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?
941 * Optionally matches the attributes list
942 *
943 * [7] - separator
944 * ([\/, >+~]+)
945 * Matches the selector list separator
946 */
947 // phpcs:ignore Generic.Files.LineLength
948 $pattern = "/(?:\:(\w+)\()?([\w:\*-]*)(?:\:(\w+)\()?(?:\#([\w-]+))?(?:|\.([\w\.-]+))?((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?(?:\))?(?:\))?([\/, >+~]+)/is";
949
950 preg_match_all(
951 $pattern,
952 trim($selector_string) . ' ', // Add final ' ' as pseudo separator
953 $matches,
954 PREG_SET_ORDER
955 );
956
957 $selectors = array();
958 $result = array();
959
960 foreach ($matches as $m) {
961 $m[0] = trim($m[0]);
962
963 // Skip NoOps
964 if ($m[0] === '' || $m[0] === '/' || $m[0] === '//') {
965 continue;
966 }
967
968 array_shift($m);
969
970 // Convert to lowercase
971 if ($this->dom->lowercase) {
972 $m[1] = strtolower($m[1]);
973 }
974
975 // Extract classes
976 if ($m[4] !== '') {
977 $m[4] = explode('.', $m[4]);
978 }
979
980 /* Extract attributes (pattern based on the pattern above!)
981
982 * [0] - full match
983 * [1] - attribute name
984 * [2] - attribute expression
985 * [3] - attribute value
986 * [4] - case sensitivity
987 *
988 * Note: Attributes can be negated with a "!" prefix to their name
989 */
990 if ($m[5] !== '') {
991 preg_match_all(
992 "/\[@?(!?[\w:-]+)(?:([!*^$|~]?=)[\"']?(.*?)[\"']?)?(?:\s+?([iIsS])?)?\]/is",
993 trim($m[5]),
994 $attributes,
995 PREG_SET_ORDER
996 );
997
998 // Replace element by array
999 $m[5] = array();
1000
1001 foreach ($attributes as $att) {
1002 // Skip empty matches
1003 if (trim($att[0]) === '') {
1004 continue;
1005 }
1006
1007 $inverted = (isset($att[1][0]) && $att[1][0] === '!');
1008 $m[5][] = array(
1009 $inverted ? substr($att[1], 1) : $att[1], // Name
1010 (isset($att[2])) ? $att[2] : '', // Expression
1011 (isset($att[3])) ? $att[3] : '', // Value
1012 $inverted, // Inverted Flag
1013 (isset($att[4])) ? strtolower($att[4]) : '', // Case-Sensitivity
1014 );
1015 }
1016 }
1017
1018 // Sanitize Separator
1019 if ($m[6] !== '' && trim($m[6]) === '') { // Descendant Separator
1020 $m[6] = ' ';
1021 } else { // Other Separator
1022 $m[6] = trim($m[6]);
1023 }
1024
1025 // Clear Separator if it's a Selector List
1026 if ($is_list = ($m[6] === ',')) {
1027 $m[6] = '';
1028 }
1029
1030 $result[] = $m;
1031
1032 if ($is_list) { // Selector List
1033 $selectors[] = $result;
1034 $result = array();
1035 }
1036 }
1037
1038 if (count($result) > 0) {
1039 $selectors[] = $result;
1040 }
1041 return $selectors;
1042 }
1043
1044 function __get($name)
1045 {
1046 if (isset($this->attr[$name])) {
1047 return $this->convert_text($this->attr[$name]);
1048 }
1049
1050 switch ($name) {
1051 case 'outertext':
1052 return $this->outertext();
1053 case 'innertext':
1054 return $this->innertext();
1055 case 'plaintext':
1056 return $this->text();
1057 case 'xmltext':
1058 return $this->xmltext();
1059 }
1060
1061 return false;
1062 }
1063
1064 function __set($name, $value)
1065 {
1066 switch ($name) {
1067 case 'outertext':
1068 $this->_[self::HDOM_INFO_OUTER] = $value;
1069 break;
1070 case 'innertext':
1071 if (isset($this->_[self::HDOM_INFO_TEXT])) {
1072 $this->_[self::HDOM_INFO_TEXT] = '';
1073 }
1074 $this->_[self::HDOM_INFO_INNER] = $value;
1075 break;
1076 default:
1077 $this->attr[$name] = $value;
1078 }
1079 }
1080
1081 function __isset($name)
1082 {
1083 switch ($name) {
1084 case 'outertext':
1085 return true;
1086 case 'innertext':
1087 return true;
1088 case 'plaintext':
1089 return true;
1090 }
1091
1092 return isset($this->attr[$name]);
1093 }
1094
1095 function __unset($name)
1096 {
1097 if (isset($this->attr[$name])) {
1098 unset($this->attr[$name]);
1099 }
1100 }
1101
1102 function convert_text($text)
1103 {
1104 $converted_text = $text;
1105
1106 $sourceCharset = '';
1107 $targetCharset = '';
1108
1109 if ($this->dom) {
1110 $sourceCharset = strtoupper($this->dom->_charset);
1111 $targetCharset = strtoupper($this->dom->_target_charset);
1112 }
1113
1114 if (!empty($sourceCharset) && !empty($targetCharset)) {
1115 if (strtoupper($sourceCharset) === strtoupper($targetCharset)) {
1116 $converted_text = $text;
1117 } elseif ((strtoupper($targetCharset) === 'UTF-8') && (self::is_utf8($text))) {
1118 Debug::log_once('The source charset was incorrectly detected as ' . $sourceCharset . ' but should have been UTF-8');
1119 $converted_text = $text;
1120 } else {
1121 $converted_text = iconv($sourceCharset, $targetCharset, $text);
1122 }
1123 }
1124
1125 // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output.
1126 if ($targetCharset === 'UTF-8') {
1127 if (substr($converted_text, 0, 3) === "\xef\xbb\xbf") {
1128 $converted_text = substr($converted_text, 3);
1129 }
1130
1131 if (substr($converted_text, -3) === "\xef\xbb\xbf") {
1132 $converted_text = substr($converted_text, 0, -3);
1133 }
1134 }
1135
1136 return $converted_text;
1137 }
1138
1139 static function is_utf8($str)
1140 {
1141 $c = 0;
1142 $b = 0;
1143 $bits = 0;
1144 $len = strlen($str);
1145 for ($i = 0; $i < $len; $i++) {
1146 $c = ord($str[$i]);
1147 if ($c > 128) {
1148 if (($c >= 254)) {
1149 return false;
1150 } elseif ($c >= 252) {
1151 $bits = 6;
1152 } elseif ($c >= 248) {
1153 $bits = 5;
1154 } elseif ($c >= 240) {
1155 $bits = 4;
1156 } elseif ($c >= 224) {
1157 $bits = 3;
1158 } elseif ($c >= 192) {
1159 $bits = 2;
1160 } else {
1161 return false;
1162 }
1163 if (($i + $bits) > $len) {
1164 return false;
1165 }
1166 while ($bits > 1) {
1167 $i++;
1168 $b = ord($str[$i]);
1169 if ($b < 128 || $b > 191) {
1170 return false;
1171 }
1172 $bits--;
1173 }
1174 }
1175 }
1176 return true;
1177 }
1178
1179 function get_display_size()
1180 {
1181 $width = -1;
1182 $height = -1;
1183
1184 if ($this->tag !== 'img') {
1185 return false;
1186 }
1187
1188 // See if there is aheight or width attribute in the tag itself.
1189 if (isset($this->attr['width'])) {
1190 $width = $this->attr['width'];
1191 }
1192
1193 if (isset($this->attr['height'])) {
1194 $height = $this->attr['height'];
1195 }
1196
1197 // Now look for an inline style.
1198 if (isset($this->attr['style'])) {
1199 // Thanks to user gnarf from stackoverflow for this regular expression.
1200 $attributes = array();
1201
1202 preg_match_all(
1203 '/([\w-]+)\s*:\s*([^;]+)\s*;?/',
1204 $this->attr['style'],
1205 $matches,
1206 PREG_SET_ORDER
1207 );
1208
1209 foreach ($matches as $match) {
1210 $attributes[$match[1]] = $match[2];
1211 }
1212
1213 // If there is a width in the style attributes:
1214 if (isset($attributes['width']) && $width == -1) {
1215 // check that the last two characters are px (pixels)
1216 if (strtolower(substr($attributes['width'], -2)) === 'px') {
1217 $proposed_width = substr($attributes['width'], 0, -2);
1218 // Now make sure that it's an integer and not something stupid.
1219 if (filter_var($proposed_width, FILTER_VALIDATE_INT)) {
1220 $width = $proposed_width;
1221 }
1222 }
1223 }
1224
1225 // If there is a width in the style attributes:
1226 if (isset($attributes['height']) && $height == -1) {
1227 // check that the last two characters are px (pixels)
1228 if (strtolower(substr($attributes['height'], -2)) == 'px') {
1229 $proposed_height = substr($attributes['height'], 0, -2);
1230 // Now make sure that it's an integer and not something stupid.
1231 if (filter_var($proposed_height, FILTER_VALIDATE_INT)) {
1232 $height = $proposed_height;
1233 }
1234 }
1235 }
1236 }
1237
1238 // Future enhancement:
1239 // Look in the tag to see if there is a class or id specified that has
1240 // a height or width attribute to it.
1241
1242 // Far future enhancement
1243 // Look at all the parent tags of this image to see if they specify a
1244 // class or id that has an img selector that specifies a height or width
1245 // Note that in this case, the class or id will have the img subselector
1246 // for it to apply to the image.
1247
1248 // ridiculously far future development
1249 // If the class or id is specified in a SEPARATE css file thats not on
1250 // the page, go get it and do what we were just doing for the ones on
1251 // the page.
1252
1253 $result = array(
1254 'height' => $height,
1255 'width' => $width
1256 );
1257
1258 return $result;
1259 }
1260
1261 function save($filepath = '')
1262 {
1263 $ret = $this->outertext();
1264
1265 if ($filepath !== '') {
1266 file_put_contents($filepath, $ret, LOCK_EX);
1267 }
1268
1269 return $ret;
1270 }
1271
1272 function addClass($class)
1273 {
1274 if (is_string($class)) {
1275 $class = explode(' ', $class);
1276 }
1277
1278 if (is_array($class)) {
1279 foreach ($class as $c) {
1280 if (isset($this->class)) {
1281 if ($this->hasClass($c)) {
1282 continue;
1283 } else {
1284 $this->class .= ' ' . $c;
1285 }
1286 } else {
1287 $this->class = $c;
1288 }
1289 }
1290 }
1291 }
1292
1293 function hasClass($class)
1294 {
1295 if (is_string($class)) {
1296 if (isset($this->class)) {
1297 return in_array($class, explode(' ', $this->class), true);
1298 }
1299 }
1300
1301 return false;
1302 }
1303
1304 function removeClass($class = null)
1305 {
1306 if (!isset($this->class)) {
1307 return;
1308 }
1309
1310 if (is_null($class)) {
1311 $this->removeAttribute('class');
1312 return;
1313 }
1314
1315 if (is_string($class)) {
1316 $class = explode(' ', $class);
1317 }
1318
1319 if (is_array($class)) {
1320 $class = array_diff(explode(' ', $this->class), $class);
1321 if (empty($class)) {
1322 $this->removeAttribute('class');
1323 } else {
1324 $this->class = implode(' ', $class);
1325 }
1326 }
1327 }
1328
1329 function getAllAttributes()
1330 {
1331 return $this->attr;
1332 }
1333
1334 function getAttribute($name)
1335 {
1336 return $this->$name;
1337 }
1338
1339 function setAttribute($name, $value)
1340 {
1341 $this->$name = $value;
1342 }
1343
1344 function hasAttribute($name)
1345 {
1346 return isset($this->$name);
1347 }
1348
1349 function removeAttribute($name)
1350 {
1351 unset($this->$name);
1352 }
1353
1354 function remove()
1355 {
1356 if ($this->parent) {
1357 $this->parent->removeChild($this);
1358 }
1359 }
1360
1361 function removeChild($node)
1362 {
1363 foreach ($node->children as $child) {
1364 $node->removeChild($child);
1365 }
1366
1367 // No need to re-index node->children because it is about to be removed!
1368
1369 foreach ($node->nodes as $entity) {
1370 $enidx = array_search($entity, $node->nodes, true);
1371 $edidx = array_search($entity, $node->dom->nodes, true);
1372
1373 if ($enidx !== false) {
1374 unset($node->nodes[$enidx]);
1375 }
1376
1377 if ($edidx !== false) {
1378 unset($node->dom->nodes[$edidx]);
1379 }
1380 }
1381
1382 // No need to re-index node->nodes because it is about to be removed!
1383
1384 $nidx = array_search($node, $this->nodes, true);
1385 $cidx = array_search($node, $this->children, true);
1386 $didx = array_search($node, $this->dom->nodes, true);
1387
1388 if ($nidx !== false) {
1389 unset($this->nodes[$nidx]);
1390 }
1391
1392 $this->nodes = array_values($this->nodes);
1393
1394 if ($cidx !== false) {
1395 unset($this->children[$cidx]);
1396 }
1397
1398 $this->children = array_values($this->children);
1399
1400 if ($didx !== false) {
1401 unset($this->dom->nodes[$didx]);
1402 }
1403
1404 // Do not re-index dom->nodes because nodes point to other nodes in the
1405 // array explicitly!
1406
1407 $node->clear();
1408 }
1409
1410 function getElementById($id)
1411 {
1412 return $this->find("#$id", 0);
1413 }
1414
1415 function getElementsById($id, $idx = null)
1416 {
1417 return $this->find("#$id", $idx);
1418 }
1419
1420 function getElementByTagName($name)
1421 {
1422 return $this->find($name, 0);
1423 }
1424
1425 function getElementsByTagName($name, $idx = null)
1426 {
1427 return $this->find($name, $idx);
1428 }
1429
1430 function parentNode()
1431 {
1432 return $this->parent();
1433 }
1434
1435 function childNodes($idx = -1)
1436 {
1437 if ($idx === -1) {
1438 return $this->children;
1439 }
1440
1441 if (isset($this->children[$idx])) {
1442 return $this->children[$idx];
1443 }
1444
1445 return null;
1446 }
1447
1448 function firstChild()
1449 {
1450 if (count($this->children) > 0) {
1451 return $this->children[0];
1452 }
1453 return null;
1454 }
1455
1456 function lastChild()
1457 {
1458 if (count($this->children) > 0) {
1459 return end($this->children);
1460 }
1461 return null;
1462 }
1463
1464 function nextSibling()
1465 {
1466 if ($this->parent === null) {
1467 return null;
1468 }
1469
1470 $idx = array_search($this, $this->parent->children, true);
1471
1472 if ($idx !== false && isset($this->parent->children[$idx + 1])) {
1473 return $this->parent->children[$idx + 1];
1474 }
1475
1476 return null;
1477 }
1478
1479 function previousSibling()
1480 {
1481 if ($this->parent === null) {
1482 return null;
1483 }
1484
1485 $idx = array_search($this, $this->parent->children, true);
1486
1487 if ($idx !== false && $idx > 0) {
1488 return $this->parent->children[$idx - 1];
1489 }
1490
1491 return null;
1492 }
1493
1494 function hasChildNodes()
1495 {
1496 return !empty($this->children);
1497 }
1498
1499 function nodeName()
1500 {
1501 return $this->tag;
1502 }
1503
1504 function appendChild($node)
1505 {
1506 $node->parent = $this;
1507 $this->nodes[] = $node;
1508 $this->children[] = $node;
1509
1510 if ($this->dom) { // Attach current node to DOM (recursively)
1511 $children = array($node);
1512
1513 while ($children) {
1514 $child = array_pop($children);
1515 $children = array_merge($children, $child->children);
1516
1517 $this->dom->nodes[] = $child;
1518 $child->dom = $this->dom;
1519 $child->_[self::HDOM_INFO_BEGIN] = count($this->dom->nodes) - 1;
1520 $child->_[self::HDOM_INFO_END] = $child->_[self::HDOM_INFO_BEGIN];
1521 }
1522
1523 $this->dom->root->_[self::HDOM_INFO_END] = count($this->dom->nodes) - 1;
1524 }
1525
1526 return $this;
1527 }
1528 }
1529 //phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
1530