PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.6
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.6
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / simplehtmldom / simplehtmldom / HtmlNode.php

HtmlNode.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.2.6, at vendor/simplehtmldom/simplehtmldom/HtmlNode.php

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