PluginProbe
MaxButtons – Create buttons / 7.13
MaxButtons – Create buttons v7.13
6.23 6.24 6.25 6.26 6.26.1 6.27 6.28 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.0 7.1 7.1.1 7.1.2 7.1.3 7.10 7.11 7.13 7.13.1 7.13.2 7.13.3 All 100 releases
maxbuttons / assets / libraries / simplehtmldom / simple_html_dom.php

simple_html_dom.php in MaxButtons – Create buttons 7.13, at assets/libraries/simplehtmldom/simple_html_dom.php

1,750 lines 53.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Website: http://sourceforge.net/projects/simplehtmldom/
4 * Additional projects that may be used: http://sourceforge.net/projects/debugobject/
5 * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/)
6 * Contributions by:
7 * Yousuke Kumakura (Attribute filters)
8 * Vadim Voituk (Negative indexes supports of "find" method)
9 * Antcs (Constructor with automatically load contents either text or file/url)
10 *
11 * all affected sections have comments starting with "PaperG"
12 *
13 * Paperg - Added case insensitive testing of the value of the selector.
14 * Paperg - Added tag_start for the starting index of tags - NOTE: This works but not accurately.
15 * This tag_start gets counted AFTER \r\n have been crushed out, and after the remove_noice calls so it will not reflect the REAL position of the tag in the source,
16 * it will almost always be smaller by some amount.
17 * We use this to determine how far into the file the tag in question is. This "percentage will never be accurate as the $dom->size is the "real" number of bytes the dom was created from.
18 * but for most purposes, it's a really good estimation.
19 * Paperg - Added the forceTagsClosed to the dom constructor. Forcing tags closed is great for malformed html, but it CAN lead to parsing errors.
20 * Allow the user to tell us how much they trust the html.
21 * Paperg add the text and plaintext to the selectors for the find syntax. plaintext implies text in the innertext of a node. text implies that the tag is a text node.
22 * This allows for us to find tags based on the text they contain.
23 * Create find_ancestor_tag to see if a tag is - at any level - inside of another specific tag.
24 * Paperg: added parse_charset so that we know about the character set of the source document.
25 * NOTE: If the user's system has a routine called get_last_retrieve_url_contents_content_type availalbe, we will assume it's returning the content-type header from the
26 * last transfer or curl_exec, and we will parse that and use it in preference to any other method of charset detection.
27 *
28 * Found infinite loop in the case of broken html in restore_noise. Rewrote to protect from that.
29 * PaperG (John Schlick) Added get_display_size for "IMG" tags.
30 *
31 * Licensed under The MIT License
32 * Redistributions of files must retain the above copyright notice.
33 *
34 * @author S.C. Chen <me578022@gmail.com>
35 * @author John Schlick
36 * @author Rus Carroll
37 * @version 1.5 ($Rev: 210 $)
38 * @package PlaceLocalInclude
39 * @subpackage simple_html_dom
40 */
41
42 /**
43 * All of the Defines for the classes below.
44 * @author S.C. Chen <me578022@gmail.com>
45 */
46
47
48 define('HDOM_TYPE_ELEMENT', 1);
49 define('HDOM_TYPE_COMMENT', 2);
50 define('HDOM_TYPE_TEXT', 3);
51 define('HDOM_TYPE_ENDTAG', 4);
52 define('HDOM_TYPE_ROOT', 5);
53 define('HDOM_TYPE_UNKNOWN', 6);
54 define('HDOM_QUOTE_DOUBLE', 0);
55 define('HDOM_QUOTE_SINGLE', 1);
56 define('HDOM_QUOTE_NO', 3);
57 define('HDOM_INFO_BEGIN', 0);
58 define('HDOM_INFO_END', 1);
59 define('HDOM_INFO_QUOTE', 2);
60 define('HDOM_INFO_SPACE', 3);
61 define('HDOM_INFO_TEXT', 4);
62 define('HDOM_INFO_INNER', 5);
63 define('HDOM_INFO_OUTER', 6);
64 define('HDOM_INFO_ENDSPACE',7);
65 define('DEFAULT_TARGET_CHARSET', 'UTF-8');
66 define('DEFAULT_BR_TEXT', "\r\n");
67 define('DEFAULT_SPAN_TEXT', " ");
68 define('MAX_FILE_SIZE', 600000);
69
70
71 // helper functions
72 // -----------------------------------------------------------------------------
73 // get html dom from file
74 // $maxlen is defined in the code as PHP_STREAM_COPY_ALL which is defined as -1.
75 function file_get_html($url, $use_include_path = false, $context=null, $offset = -1, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
76 {
77 // We DO force the tags to be terminated.
78 $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $stripRN, $defaultBRText, $defaultSpanText);
79 // For sourceforge users: uncomment the next line and comment the retreive_url_contents line 2 lines down if it is not already done.
80 $contents = file_get_contents($url, $use_include_path, $context, $offset);
81 // Paperg - use our own mechanism for getting the contents as we want to control the timeout.
82 //$contents = retrieve_url_contents($url);
83 if (empty($contents) || strlen($contents) > MAX_FILE_SIZE)
84 {
85 return false;
86 }
87 // The second parameter can force the selectors to all be lowercase.
88 $dom->load($contents, $lowercase, $stripRN);
89 return $dom;
90 }
91
92 // get html dom from string
93 function str_get_html($str, $lowercase=true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
94 {
95 $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $stripRN, $defaultBRText, $defaultSpanText);
96 if (empty($str) || strlen($str) > MAX_FILE_SIZE)
97 {
98 $dom->clear();
99 return false;
100 }
101 $dom->load($str, $lowercase, $stripRN);
102 return $dom;
103 }
104
105 // dump html dom tree
106 function dump_html_tree($node, $show_attr=true, $deep=0)
107 {
108 $node->dump($node);
109 }
110
111
112 /**
113 * simple html dom node
114 * PaperG - added ability for "find" routine to lowercase the value of the selector.
115 * PaperG - added $tag_start to track the start position of the tag in the total byte index
116 *
117 * @package PlaceLocalInclude
118 */
119 class simple_html_dom_node
120 {
121 public $nodetype = HDOM_TYPE_TEXT;
122 public $tag = 'text';
123 public $attr = array();
124 public $children = array();
125 public $nodes = array();
126 public $parent = null;
127 // The "info" array - see HDOM_INFO_... for what each element contains.
128 public $_ = array();
129 public $tag_start = 0;
130 private $dom = null;
131
132 function __construct($dom)
133 {
134 $this->dom = $dom;
135 $dom->nodes[] = $this;
136 }
137
138 function __destruct()
139 {
140 $this->clear();
141 }
142
143 function __toString()
144 {
145 return $this->outertext();
146 }
147
148 // clean up memory due to php5 circular references memory leak...
149 function clear()
150 {
151 $this->dom = null;
152 $this->nodes = null;
153 $this->parent = null;
154 $this->children = null;
155 }
156
157 // dump node's tree
158 function dump($show_attr=true, $deep=0)
159 {
160 $lead = str_repeat(' ', $deep);
161
162 echo $lead.$this->tag;
163 if ($show_attr && count($this->attr)>0)
164 {
165 echo '(';
166 foreach ($this->attr as $k=>$v)
167 echo "[$k]=>\"".$this->$k.'", ';
168 echo ')';
169 }
170 echo "\n";
171
172 if ($this->nodes)
173 {
174 foreach ($this->nodes as $c)
175 {
176 $c->dump($show_attr, $deep+1);
177 }
178 }
179 }
180
181
182 // Debugging function to dump a single dom node with a bunch of information about it.
183 function dump_node($echo=true)
184 {
185
186 $string = $this->tag;
187 if (count($this->attr)>0)
188 {
189 $string .= '(';
190 foreach ($this->attr as $k=>$v)
191 {
192 $string .= "[$k]=>\"".$this->$k.'", ';
193 }
194 $string .= ')';
195 }
196 if (count($this->_)>0)
197 {
198 $string .= ' $_ (';
199 foreach ($this->_ as $k=>$v)
200 {
201 if (is_array($v))
202 {
203 $string .= "[$k]=>(";
204 foreach ($v as $k2=>$v2)
205 {
206 $string .= "[$k2]=>\"".$v2.'", ';
207 }
208 $string .= ")";
209 } else {
210 $string .= "[$k]=>\"".$v.'", ';
211 }
212 }
213 $string .= ")";
214 }
215
216 if (isset($this->text))
217 {
218 $string .= " text: (" . $this->text . ")";
219 }
220
221 $string .= " HDOM_INNER_INFO: '";
222 if (isset($node->_[HDOM_INFO_INNER]))
223 {
224 $string .= $node->_[HDOM_INFO_INNER] . "'";
225 }
226 else
227 {
228 $string .= ' NULL ';
229 }
230
231 $string .= " children: " . count($this->children);
232 $string .= " nodes: " . count($this->nodes);
233 $string .= " tag_start: " . $this->tag_start;
234 $string .= "\n";
235
236 if ($echo)
237 {
238 echo $string;
239 return;
240 }
241 else
242 {
243 return $string;
244 }
245 }
246
247 // returns the parent of node
248 // If a node is passed in, it will reset the parent of the current node to that one.
249 function parent($parent=null)
250 {
251 // I am SURE that this doesn't work properly.
252 // It fails to unset the current node from it's current parents nodes or children list first.
253 if ($parent !== null)
254 {
255 $this->parent = $parent;
256 $this->parent->nodes[] = $this;
257 $this->parent->children[] = $this;
258 }
259
260 return $this->parent;
261 }
262
263 // verify that node has children
264 function has_child()
265 {
266 return !empty($this->children);
267 }
268
269 // returns children of node
270 function children($idx=-1)
271 {
272 if ($idx===-1)
273 {
274 return $this->children;
275 }
276 if (isset($this->children[$idx]))
277 {
278 return $this->children[$idx];
279 }
280 return null;
281 }
282
283 // returns the first child of node
284 function first_child()
285 {
286 if (count($this->children)>0)
287 {
288 return $this->children[0];
289 }
290 return null;
291 }
292
293 // returns the last child of node
294 function last_child()
295 {
296 if (($count=count($this->children))>0)
297 {
298 return $this->children[$count-1];
299 }
300 return null;
301 }
302
303 // returns the next sibling of node
304 function next_sibling()
305 {
306 if ($this->parent===null)
307 {
308 return null;
309 }
310
311 $idx = 0;
312 $count = count($this->parent->children);
313 while ($idx<$count && $this!==$this->parent->children[$idx])
314 {
315 ++$idx;
316 }
317 if (++$idx>=$count)
318 {
319 return null;
320 }
321 return $this->parent->children[$idx];
322 }
323
324 // returns the previous sibling of node
325 function prev_sibling()
326 {
327 if ($this->parent===null) return null;
328 $idx = 0;
329 $count = count($this->parent->children);
330 while ($idx<$count && $this!==$this->parent->children[$idx])
331 ++$idx;
332 if (--$idx<0) return null;
333 return $this->parent->children[$idx];
334 }
335
336 // function to locate a specific ancestor tag in the path to the root.
337 function find_ancestor_tag($tag)
338 {
339 global $debug_object;
340 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
341
342 // Start by including ourselves in the comparison.
343 $returnDom = $this;
344
345 while (!is_null($returnDom))
346 {
347 if (is_object($debug_object)) { $debug_object->debug_log(2, "Current tag is: " . $returnDom->tag); }
348
349 if ($returnDom->tag == $tag)
350 {
351 break;
352 }
353 $returnDom = $returnDom->parent;
354 }
355 return $returnDom;
356 }
357
358 // get dom node's inner html
359 function innertext()
360 {
361 if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
362 if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
363
364 $ret = '';
365 foreach ($this->nodes as $n)
366 $ret .= $n->outertext();
367 return $ret;
368 }
369
370 // get dom node's outer text (with tag)
371 function outertext()
372 {
373 global $debug_object;
374 if (is_object($debug_object))
375 {
376 $text = '';
377 if ($this->tag == 'text')
378 {
379 if (!empty($this->text))
380 {
381 $text = " with text: " . $this->text;
382 }
383 }
384 $debug_object->debug_log(1, 'Innertext of tag: ' . $this->tag . $text);
385 }
386
387 if ($this->tag==='root') return $this->innertext();
388
389 // trigger callback
390 if ($this->dom && $this->dom->callback!==null)
391 {
392 call_user_func_array($this->dom->callback, array($this));
393 }
394
395 if (isset($this->_[HDOM_INFO_OUTER])) return $this->_[HDOM_INFO_OUTER];
396 if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
397
398 // render begin tag
399 if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]])
400 {
401 $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup();
402 } else {
403 $ret = "";
404 }
405
406 // render inner text
407 if (isset($this->_[HDOM_INFO_INNER]))
408 {
409 // If it's a br tag... don't return the HDOM_INNER_INFO that we may or may not have added.
410 if ($this->tag != "br")
411 {
412 $ret .= $this->_[HDOM_INFO_INNER];
413 }
414 } else {
415 if ($this->nodes)
416 {
417 foreach ($this->nodes as $n)
418 {
419 $ret .= $this->convert_text($n->outertext());
420 }
421 }
422 }
423
424 // render end tag
425 if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END]!=0)
426 $ret .= '</'.$this->tag.'>';
427 return $ret;
428 }
429
430 // get dom node's plain text
431 function text()
432 {
433 if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
434 switch ($this->nodetype)
435 {
436 case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
437 case HDOM_TYPE_COMMENT: return '';
438 case HDOM_TYPE_UNKNOWN: return '';
439 }
440 if (strcasecmp($this->tag, 'script')===0) return '';
441 if (strcasecmp($this->tag, 'style')===0) return '';
442
443 $ret = '';
444 // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed for some span tags, and some p tags) $this->nodes is set to NULL.
445 // NOTE: This indicates that there is a problem where it's set to NULL without a clear happening.
446 // WHY is this happening?
447 if (!is_null($this->nodes))
448 {
449 foreach ($this->nodes as $n)
450 {
451 $ret .= $this->convert_text($n->text());
452 }
453
454 // If this node is a span... add a space at the end of it so multiple spans don't run into each other. This is plaintext after all.
455 if ($this->tag == "span")
456 {
457 $ret .= $this->dom->default_span_text;
458 }
459
460
461 }
462 return $ret;
463 }
464
465 function xmltext()
466 {
467 $ret = $this->innertext();
468 $ret = str_ireplace('<![CDATA[', '', $ret);
469 $ret = str_replace(']]>', '', $ret);
470 return $ret;
471 }
472
473 // build node's text with tag
474 function makeup()
475 {
476 // text, comment, unknown
477 if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
478
479 $ret = '<'.$this->tag;
480 $i = -1;
481
482 foreach ($this->attr as $key=>$val)
483 {
484 ++$i;
485
486 // skip removed attribute
487 if ($val===null || $val===false)
488 continue;
489
490 $ret .= $this->_[HDOM_INFO_SPACE][$i][0];
491 //no value attr: nowrap, checked selected...
492 if ($val===true)
493 $ret .= $key;
494 else {
495 switch ($this->_[HDOM_INFO_QUOTE][$i])
496 {
497 case HDOM_QUOTE_DOUBLE: $quote = '"'; break;
498 case HDOM_QUOTE_SINGLE: $quote = '\''; break;
499 default: $quote = '';
500 }
501 $ret .= $key.$this->_[HDOM_INFO_SPACE][$i][1].'='.$this->_[HDOM_INFO_SPACE][$i][2].$quote.$val.$quote;
502 }
503 }
504 $ret = $this->dom->restore_noise($ret);
505 return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>';
506 }
507
508 // find elements by css selector
509 //PaperG - added ability for find to lowercase the value of the selector.
510 function find($selector, $idx=null, $lowercase=false)
511 {
512 $selectors = $this->parse_selector($selector);
513 if (($count=count($selectors))===0) return array();
514 $found_keys = array();
515
516 // find each selector
517 for ($c=0; $c<$count; ++$c)
518 {
519 // The change on the below line was documented on the sourceforge code tracker id 2788009
520 // used to be: if (($levle=count($selectors[0]))===0) return array();
521 if (($levle=count($selectors[$c]))===0) return array();
522 if (!isset($this->_[HDOM_INFO_BEGIN])) return array();
523
524 $head = array($this->_[HDOM_INFO_BEGIN]=>1);
525
526 // handle descendant selectors, no recursive!
527 for ($l=0; $l<$levle; ++$l)
528 {
529 $ret = array();
530 foreach ($head as $k=>$v)
531 {
532 $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k];
533 //PaperG - Pass this optional parameter on to the seek function.
534 $n->seek($selectors[$c][$l], $ret, $lowercase);
535 }
536 $head = $ret;
537 }
538
539 foreach ($head as $k=>$v)
540 {
541 if (!isset($found_keys[$k]))
542 {
543 $found_keys[$k] = 1;
544 }
545 }
546 }
547
548 // sort keys
549 ksort($found_keys);
550
551 $found = array();
552 foreach ($found_keys as $k=>$v)
553 $found[] = $this->dom->nodes[$k];
554
555 // return nth-element or array
556 if (is_null($idx)) return $found;
557 else if ($idx<0) $idx = count($found) + $idx;
558 return (isset($found[$idx])) ? $found[$idx] : null;
559 }
560
561 // seek for given conditions
562 // PaperG - added parameter to allow for case insensitive testing of the value of a selector.
563 protected function seek($selector, &$ret, $lowercase=false)
564 {
565 global $debug_object;
566 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
567
568 list($tag, $key, $val, $exp, $no_key) = $selector;
569
570 // xpath index
571 if ($tag && $key && is_numeric($key))
572 {
573 $count = 0;
574 foreach ($this->children as $c)
575 {
576 if ($tag==='*' || $tag===$c->tag) {
577 if (++$count==$key) {
578 $ret[$c->_[HDOM_INFO_BEGIN]] = 1;
579 return;
580 }
581 }
582 }
583 return;
584 }
585
586 $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0;
587 if ($end==0) {
588 $parent = $this->parent;
589 while (!isset($parent->_[HDOM_INFO_END]) && $parent!==null) {
590 $end -= 1;
591 $parent = $parent->parent;
592 }
593 $end += $parent->_[HDOM_INFO_END];
594 }
595
596 for ($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) {
597 $node = $this->dom->nodes[$i];
598
599 $pass = true;
600
601 if ($tag==='*' && !$key) {
602 if (in_array($node, $this->children, true))
603 $ret[$i] = 1;
604 continue;
605 }
606
607 // compare tag
608 if ($tag && $tag!=$node->tag && $tag!=='*') {$pass=false;}
609 // compare key
610 if ($pass && $key) {
611 if ($no_key) {
612 if (isset($node->attr[$key])) $pass=false;
613 } else {
614 if (($key != "plaintext") && !isset($node->attr[$key])) $pass=false;
615 }
616 }
617 // compare value
618 if ($pass && $key && $val && $val!=='*') {
619 // If they have told us that this is a "plaintext" search then we want the plaintext of the node - right?
620 if ($key == "plaintext") {
621 // $node->plaintext actually returns $node->text();
622 $nodeKeyValue = $node->text();
623 } else {
624 // this is a normal search, we want the value of that attribute of the tag.
625 $nodeKeyValue = $node->attr[$key];
626 }
627 if (is_object($debug_object)) {$debug_object->debug_log(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);}
628
629 //PaperG - If lowercase is set, do a case insensitive test of the value of the selector.
630 if ($lowercase) {
631 $check = $this->match($exp, strtolower($val), strtolower($nodeKeyValue));
632 } else {
633 $check = $this->match($exp, $val, $nodeKeyValue);
634 }
635 if (is_object($debug_object)) {$debug_object->debug_log(2, "after match: " . ($check ? "true" : "false"));}
636
637 // handle multiple class
638 if (!$check && strcasecmp($key, 'class')===0) {
639 foreach (explode(' ',$node->attr[$key]) as $k) {
640 // Without this, there were cases where leading, trailing, or double spaces lead to our comparing blanks - bad form.
641 if (!empty($k)) {
642 if ($lowercase) {
643 $check = $this->match($exp, strtolower($val), strtolower($k));
644 } else {
645 $check = $this->match($exp, $val, $k);
646 }
647 if ($check) break;
648 }
649 }
650 }
651 if (!$check) $pass = false;
652 }
653 if ($pass) $ret[$i] = 1;
654 unset($node);
655 }
656 // It's passed by reference so this is actually what this function returns.
657 if (is_object($debug_object)) {$debug_object->debug_log(1, "EXIT - ret: ", $ret);}
658 }
659
660 protected function match($exp, $pattern, $value) {
661 global $debug_object;
662 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
663
664 switch ($exp) {
665 case '=':
666 return ($value===$pattern);
667 case '!=':
668 return ($value!==$pattern);
669 case '^=':
670 return preg_match("/^".preg_quote($pattern,'/')."/", $value);
671 case '$=':
672 return preg_match("/".preg_quote($pattern,'/')."$/", $value);
673 case '*=':
674 if ($pattern[0]=='/') {
675 return preg_match($pattern, $value);
676 }
677 return preg_match("/".$pattern."/i", $value);
678 }
679 return false;
680 }
681
682 protected function parse_selector($selector_string) {
683 global $debug_object;
684 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
685
686 // pattern of CSS selectors, modified from mootools
687 // Paperg: Add the colon to the attrbute, so that it properly finds <tag attr:ibute="something" > like google does.
688 // Note: if you try to look at this attribute, yo MUST use getAttribute since $dom->x:y will fail the php syntax check.
689 // Notice the \[ starting the attbute? and the @? following? This implies that an attribute can begin with an @ sign that is not captured.
690 // This implies that an html attribute specifier may start with an @ sign that is NOT captured by the expression.
691 // farther study is required to determine of this should be documented or removed.
692 // $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
693
694 // original $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
695 $pattern = "/([\w\-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w\-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
696 preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER);
697 if (is_object($debug_object)) {$debug_object->debug_log(2, "Matches Array: ", $matches);}
698
699 $selectors = array();
700 $result = array();
701
702
703 foreach ($matches as $m) {
704 $m[0] = trim($m[0]);
705 if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue;
706 // for browser generated xpath
707 if ($m[1]==='tbody') continue;
708
709 list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false);
710 if (!empty($m[2])) {$key='id'; $val=$m[2];}
711 if (!empty($m[3])) {$key='class'; $val=$m[3];}
712 if (!empty($m[4])) {$key=$m[4];}
713 if (!empty($m[5])) {$exp=$m[5];}
714 if (!empty($m[6])) {$val=$m[6];}
715
716 // convert to lowercase
717 if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);}
718 //elements that do NOT have the specified attribute
719 if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;}
720
721 $result[] = array($tag, $key, $val, $exp, $no_key);
722 if (trim($m[7])===',') {
723 $selectors[] = $result;
724 $result = array();
725 }
726 }
727 if (count($result)>0)
728 $selectors[] = $result;
729 return $selectors;
730 }
731
732 function __get($name)
733 {
734 if (isset($this->attr[$name]))
735 {
736 return $this->convert_text($this->attr[$name]);
737 }
738 switch ($name)
739 {
740 case 'outertext': return $this->outertext();
741 case 'innertext': return $this->innertext();
742 case 'plaintext': return $this->text();
743 case 'xmltext': return $this->xmltext();
744 default: return array_key_exists($name, $this->attr);
745 }
746 }
747
748 function __set($name, $value)
749 {
750 global $debug_object;
751 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
752
753 switch ($name)
754 {
755 case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
756 case 'innertext':
757 if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value;
758 return $this->_[HDOM_INFO_INNER] = $value;
759 }
760 if (!isset($this->attr[$name]))
761 {
762 $this->_[HDOM_INFO_SPACE][] = array(' ', '', '');
763 $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
764 }
765 $this->attr[$name] = $value;
766 }
767
768 function __isset($name)
769 {
770 switch ($name)
771 {
772 case 'outertext': return true;
773 case 'innertext': return true;
774 case 'plaintext': return true;
775 }
776 //no value attr: nowrap, checked selected...
777 return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]);
778 }
779
780 function __unset($name) {
781 if (isset($this->attr[$name]))
782 unset($this->attr[$name]);
783 }
784
785 // PaperG - Function to convert the text from one character set to another if the two sets are not the same.
786 function convert_text($text)
787 {
788 global $debug_object;
789 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
790
791 $converted_text = $text;
792
793 $sourceCharset = "";
794 $targetCharset = "";
795
796 if ($this->dom)
797 {
798 $sourceCharset = strtoupper($this->dom->_charset);
799 $targetCharset = strtoupper($this->dom->_target_charset);
800 }
801 if (is_object($debug_object)) {$debug_object->debug_log(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);}
802
803 if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0))
804 {
805 // Check if the reported encoding could have been incorrect and the text is actually already UTF-8
806 if ((strcasecmp($targetCharset, 'UTF-8') == 0) && ($this->is_utf8($text)))
807 {
808 $converted_text = $text;
809 }
810 else
811 {
812 $converted_text = iconv($sourceCharset, $targetCharset, $text);
813 }
814 }
815
816 // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output.
817 if ($targetCharset == 'UTF-8')
818 {
819 if (substr($converted_text, 0, 3) == "\xef\xbb\xbf")
820 {
821 $converted_text = substr($converted_text, 3);
822 }
823 if (substr($converted_text, -3) == "\xef\xbb\xbf")
824 {
825 $converted_text = substr($converted_text, 0, -3);
826 }
827 }
828
829 return $converted_text;
830 }
831
832 /**
833 * Returns true if $string is valid UTF-8 and false otherwise.
834 *
835 * @param mixed $str String to be tested
836 * @return boolean
837 */
838 static function is_utf8($str)
839 {
840 $c=0; $b=0;
841 $bits=0;
842 $len=strlen($str);
843 for($i=0; $i<$len; $i++)
844 {
845 $c=ord($str[$i]);
846 if($c > 128)
847 {
848 if(($c >= 254)) return false;
849 elseif($c >= 252) $bits=6;
850 elseif($c >= 248) $bits=5;
851 elseif($c >= 240) $bits=4;
852 elseif($c >= 224) $bits=3;
853 elseif($c >= 192) $bits=2;
854 else return false;
855 if(($i+$bits) > $len) return false;
856 while($bits > 1)
857 {
858 $i++;
859 $b=ord($str[$i]);
860 if($b < 128 || $b > 191) return false;
861 $bits--;
862 }
863 }
864 }
865 return true;
866 }
867 /*
868 function is_utf8($string)
869 {
870 //this is buggy
871 return (utf8_encode(utf8_decode($string)) == $string);
872 }
873 */
874
875 /**
876 * Function to try a few tricks to determine the displayed size of an img on the page.
877 * NOTE: This will ONLY work on an IMG tag. Returns FALSE on all other tag types.
878 *
879 * @author John Schlick
880 * @version April 19 2012
881 * @return array an array containing the 'height' and 'width' of the image on the page or -1 if we can't figure it out.
882 */
883 function get_display_size()
884 {
885 global $debug_object;
886
887 $width = -1;
888 $height = -1;
889
890 if ($this->tag !== 'img')
891 {
892 return false;
893 }
894
895 // See if there is aheight or width attribute in the tag itself.
896 if (isset($this->attr['width']))
897 {
898 $width = $this->attr['width'];
899 }
900
901 if (isset($this->attr['height']))
902 {
903 $height = $this->attr['height'];
904 }
905
906 // Now look for an inline style.
907 if (isset($this->attr['style']))
908 {
909 // Thanks to user gnarf from stackoverflow for this regular expression.
910 $attributes = array();
911 preg_match_all("/([\w-]+)\s*:\s*([^;]+)\s*;?/", $this->attr['style'], $matches, PREG_SET_ORDER);
912 foreach ($matches as $match) {
913 $attributes[$match[1]] = $match[2];
914 }
915
916 // If there is a width in the style attributes:
917 if (isset($attributes['width']) && $width == -1)
918 {
919 // check that the last two characters are px (pixels)
920 if (strtolower(substr($attributes['width'], -2)) == 'px')
921 {
922 $proposed_width = substr($attributes['width'], 0, -2);
923 // Now make sure that it's an integer and not something stupid.
924 if (filter_var($proposed_width, FILTER_VALIDATE_INT))
925 {
926 $width = $proposed_width;
927 }
928 }
929 }
930
931 // If there is a width in the style attributes:
932 if (isset($attributes['height']) && $height == -1)
933 {
934 // check that the last two characters are px (pixels)
935 if (strtolower(substr($attributes['height'], -2)) == 'px')
936 {
937 $proposed_height = substr($attributes['height'], 0, -2);
938 // Now make sure that it's an integer and not something stupid.
939 if (filter_var($proposed_height, FILTER_VALIDATE_INT))
940 {
941 $height = $proposed_height;
942 }
943 }
944 }
945
946 }
947
948 // Future enhancement:
949 // Look in the tag to see if there is a class or id specified that has a height or width attribute to it.
950
951 // Far future enhancement
952 // Look at all the parent tags of this image to see if they specify a class or id that has an img selector that specifies a height or width
953 // Note that in this case, the class or id will have the img subselector for it to apply to the image.
954
955 // ridiculously far future development
956 // If the class or id is specified in a SEPARATE css file thats not on the page, go get it and do what we were just doing for the ones on the page.
957
958 $result = array('height' => $height,
959 'width' => $width);
960 return $result;
961 }
962
963 // camel naming conventions
964 function getAllAttributes() {return $this->attr;}
965 function getAttribute($name) {return $this->__get($name);}
966 function setAttribute($name, $value) {$this->__set($name, $value);}
967 function hasAttribute($name) {return $this->__isset($name);}
968 function removeAttribute($name) {$this->__set($name, null);}
969 function getElementById($id) {return $this->find("#$id", 0);}
970 function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
971 function getElementByTagName($name) {return $this->find($name, 0);}
972 function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);}
973 function parentNode() {return $this->parent();}
974 function childNodes($idx=-1) {return $this->children($idx);}
975 function firstChild() {return $this->first_child();}
976 function lastChild() {return $this->last_child();}
977 function nextSibling() {return $this->next_sibling();}
978 function previousSibling() {return $this->prev_sibling();}
979 function hasChildNodes() {return $this->has_child();}
980 function nodeName() {return $this->tag;}
981 function appendChild($node) {$node->parent($this); return $node;}
982
983 }
984
985 /**
986 * simple html dom parser
987 * Paperg - in the find routine: allow us to specify that we want case insensitive testing of the value of the selector.
988 * Paperg - change $size from protected to public so we can easily access it
989 * Paperg - added ForceTagsClosed in the constructor which tells us whether we trust the html or not. Default is to NOT trust it.
990 *
991 * @package PlaceLocalInclude
992 */
993 class simple_html_dom
994 {
995 public $root = null;
996 public $nodes = array();
997 public $callback = null;
998 public $lowercase = false;
999 // Used to keep track of how large the text was when we started.
1000 public $original_size;
1001 public $size;
1002 protected $pos;
1003 protected $doc;
1004 protected $char;
1005 protected $cursor;
1006 protected $parent;
1007 protected $noise = array();
1008 protected $token_blank = " \t\r\n";
1009 protected $token_equal = ' =/>';
1010 protected $token_slash = " />\r\n\t";
1011 protected $token_attr = ' >';
1012 // Note that this is referenced by a child node, and so it needs to be public for that node to see this information.
1013 public $_charset = '';
1014 public $_target_charset = '';
1015 protected $default_br_text = "";
1016 public $default_span_text = "";
1017
1018 // use isset instead of in_array, performance boost about 30%...
1019 protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1);
1020 protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1);
1021 // Known sourceforge issue #2977341
1022 // B tags that are not closed cause us to return everything to the end of the document.
1023 protected $optional_closing_tags = array(
1024 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1),
1025 'th'=>array('th'=>1),
1026 'td'=>array('td'=>1),
1027 'li'=>array('li'=>1),
1028 'dt'=>array('dt'=>1, 'dd'=>1),
1029 'dd'=>array('dd'=>1, 'dt'=>1),
1030 'dl'=>array('dd'=>1, 'dt'=>1),
1031 'p'=>array('p'=>1),
1032 'nobr'=>array('nobr'=>1),
1033 'b'=>array('b'=>1),
1034 'option'=>array('option'=>1),
1035 );
1036
1037 function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1038 {
1039 if ($str)
1040 {
1041 if (preg_match("/^http:\/\//i",$str) || is_file($str))
1042 {
1043 $this->load_file($str);
1044 }
1045 else
1046 {
1047 $this->load($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1048 }
1049 }
1050 // Forcing tags to be closed implies that we don't trust the html, but it can lead to parsing errors if we SHOULD trust the html.
1051 if (!$forceTagsClosed) {
1052 $this->optional_closing_array=array();
1053 }
1054 $this->_target_charset = $target_charset;
1055 }
1056
1057 function __destruct()
1058 {
1059 $this->clear();
1060 }
1061
1062 // load html from string
1063 function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1064 {
1065 global $debug_object;
1066
1067 // prepare
1068 $this->prepare($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1069 // strip out cdata
1070 $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true);
1071 // strip out comments
1072 $this->remove_noise("'<!--(.*?)-->'is");
1073 // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037
1074 // Script tags removal now preceeds style tag removal.
1075 // strip out <script> tags
1076 $this->remove_noise("'<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>'is");
1077 $this->remove_noise("'<\s*script\s*>(.*?)<\s*/\s*script\s*>'is");
1078 // strip out <style> tags
1079 $this->remove_noise("'<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>'is");
1080 $this->remove_noise("'<\s*style\s*>(.*?)<\s*/\s*style\s*>'is");
1081 // strip out preformatted tags
1082 $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is");
1083 // strip out server side scripts
1084 $this->remove_noise("'(<\?)(.*?)(\?>)'s", true);
1085 // strip smarty scripts
1086 $this->remove_noise("'(\{\w)(.*?)(\})'s", true);
1087
1088 // parsing
1089 while ($this->parse());
1090 // end
1091 $this->root->_[HDOM_INFO_END] = $this->cursor;
1092 $this->parse_charset();
1093
1094 // make load function chainable
1095 return $this;
1096
1097 }
1098
1099 // load html from file
1100 function load_file()
1101 {
1102 $args = func_get_args();
1103 $this->load(call_user_func_array('file_get_contents', $args), true);
1104 // Throw an error if we can't properly load the dom.
1105 if (($error=error_get_last())!==null) {
1106 $this->clear();
1107 return false;
1108 }
1109 }
1110
1111 // set callback function
1112 function set_callback($function_name)
1113 {
1114 $this->callback = $function_name;
1115 }
1116
1117 // remove callback function
1118 function remove_callback()
1119 {
1120 $this->callback = null;
1121 }
1122
1123 // save dom as string
1124 function save($filepath='')
1125 {
1126 $ret = $this->root->innertext();
1127 if ($filepath!=='') file_put_contents($filepath, $ret, LOCK_EX);
1128 return $ret;
1129 }
1130
1131 // find dom node by css selector
1132 // Paperg - allow us to specify that we want case insensitive testing of the value of the selector.
1133 function find($selector, $idx=null, $lowercase=false)
1134 {
1135 return $this->root->find($selector, $idx, $lowercase);
1136 }
1137
1138 // clean up memory due to php5 circular references memory leak...
1139 function clear()
1140 {
1141 foreach ($this->nodes as $n) {$n->clear(); $n = null;}
1142 // This add next line is documented in the sourceforge repository. 2977248 as a fix for ongoing memory leaks that occur even with the use of clear.
1143 if (isset($this->children)) foreach ($this->children as $n) {$n->clear(); $n = null;}
1144 if (isset($this->parent)) {$this->parent->clear(); unset($this->parent);}
1145 if (isset($this->root)) {$this->root->clear(); unset($this->root);}
1146 unset($this->doc);
1147 unset($this->noise);
1148 }
1149
1150 function dump($show_attr=true)
1151 {
1152 $this->root->dump($show_attr);
1153 }
1154
1155 // prepare HTML data and init everything
1156 protected function prepare($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1157 {
1158 $this->clear();
1159
1160 // set the length of content before we do anything to it.
1161 $this->size = strlen($str);
1162 // Save the original size of the html that we got in. It might be useful to someone.
1163 $this->original_size = $this->size;
1164
1165 //before we save the string as the doc... strip out the \r \n's if we are told to.
1166 if ($stripRN) {
1167 $str = str_replace("\r", " ", $str);
1168 $str = str_replace("\n", " ", $str);
1169
1170 // set the length of content since we have changed it.
1171 $this->size = strlen($str);
1172 }
1173
1174 $this->doc = $str;
1175 $this->pos = 0;
1176 $this->cursor = 1;
1177 $this->noise = array();
1178 $this->nodes = array();
1179 $this->lowercase = $lowercase;
1180 $this->default_br_text = $defaultBRText;
1181 $this->default_span_text = $defaultSpanText;
1182 $this->root = new simple_html_dom_node($this);
1183 $this->root->tag = 'root';
1184 $this->root->_[HDOM_INFO_BEGIN] = -1;
1185 $this->root->nodetype = HDOM_TYPE_ROOT;
1186 $this->parent = $this->root;
1187 if ($this->size>0) $this->char = $this->doc[0];
1188 }
1189
1190 // parse html content
1191 protected function parse()
1192 {
1193 if (($s = $this->copy_until_char('<'))==='')
1194 {
1195 return $this->read_tag();
1196 }
1197
1198 // text
1199 $node = new simple_html_dom_node($this);
1200 ++$this->cursor;
1201 $node->_[HDOM_INFO_TEXT] = $s;
1202 $this->link_nodes($node, false);
1203 return true;
1204 }
1205
1206 // PAPERG - dkchou - added this to try to identify the character set of the page we have just parsed so we know better how to spit it out later.
1207 // NOTE: IF you provide a routine called get_last_retrieve_url_contents_content_type which returns the CURLINFO_CONTENT_TYPE from the last curl_exec
1208 // (or the content_type header from the last transfer), we will parse THAT, and if a charset is specified, we will use it over any other mechanism.
1209 protected function parse_charset()
1210 {
1211 global $debug_object;
1212
1213 $charset = null;
1214
1215 if (function_exists('get_last_retrieve_url_contents_content_type'))
1216 {
1217 $contentTypeHeader = get_last_retrieve_url_contents_content_type();
1218 $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches);
1219 if ($success)
1220 {
1221 $charset = $matches[1];
1222 if (is_object($debug_object)) {$debug_object->debug_log(2, 'header content-type found charset of: ' . $charset);}
1223 }
1224
1225 }
1226
1227 if (empty($charset))
1228 {
1229 $el = $this->root->find('meta[http-equiv=Content-Type]',0, true);
1230 if (!empty($el))
1231 {
1232 $fullvalue = $el->content;
1233 if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag found' . $fullvalue);}
1234
1235 if (!empty($fullvalue))
1236 {
1237 $success = preg_match('/charset=(.+)/i', $fullvalue, $matches);
1238 if ($success)
1239 {
1240 $charset = $matches[1];
1241 }
1242 else
1243 {
1244 // If there is a meta tag, and they don't specify the character set, research says that it's typically ISO-8859-1
1245 if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag couldn\'t be parsed. using iso-8859 default.');}
1246 $charset = 'ISO-8859-1';
1247 }
1248 }
1249 }
1250 }
1251
1252 // If we couldn't find a charset above, then lets try to detect one based on the text we got...
1253 if (empty($charset))
1254 {
1255 // Use this in case mb_detect_charset isn't installed/loaded on this machine.
1256 $charset = false;
1257 if (function_exists('mb_detect_encoding'))
1258 {
1259 // Have php try to detect the encoding from the text given to us.
1260 $charset = mb_detect_encoding($this->root->plaintext . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) );
1261 if (is_object($debug_object)) {$debug_object->debug_log(2, 'mb_detect found: ' . $charset);}
1262 }
1263
1264 // and if this doesn't work... then we need to just wrongheadedly assume it's UTF-8 so that we can move on - cause this will usually give us most of what we need...
1265 if ($charset === false)
1266 {
1267 if (is_object($debug_object)) {$debug_object->debug_log(2, 'since mb_detect failed - using default of utf-8');}
1268 $charset = 'UTF-8';
1269 }
1270 }
1271
1272 // Since CP1252 is a superset, if we get one of it's subsets, we want it instead.
1273 if ((strtolower($charset) == strtolower('ISO-8859-1')) || (strtolower($charset) == strtolower('Latin1')) || (strtolower($charset) == strtolower('Latin-1')))
1274 {
1275 if (is_object($debug_object)) {$debug_object->debug_log(2, 'replacing ' . $charset . ' with CP1252 as its a superset');}
1276 $charset = 'CP1252';
1277 }
1278
1279 if (is_object($debug_object)) {$debug_object->debug_log(1, 'EXIT - ' . $charset);}
1280
1281 return $this->_charset = $charset;
1282 }
1283
1284 // read tag info
1285 protected function read_tag()
1286 {
1287 if ($this->char!=='<')
1288 {
1289 $this->root->_[HDOM_INFO_END] = $this->cursor;
1290 return false;
1291 }
1292 $begin_tag_pos = $this->pos;
1293 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1294
1295 // end tag
1296 if ($this->char==='/')
1297 {
1298 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1299 // This represents the change in the simple_html_dom trunk from revision 180 to 181.
1300 // $this->skip($this->token_blank_t);
1301 $this->skip($this->token_blank);
1302 $tag = $this->copy_until_char('>');
1303
1304 // skip attributes in end tag
1305 if (($pos = strpos($tag, ' '))!==false)
1306 $tag = substr($tag, 0, $pos);
1307
1308 $parent_lower = strtolower($this->parent->tag);
1309 $tag_lower = strtolower($tag);
1310
1311 if ($parent_lower!==$tag_lower)
1312 {
1313 if (isset($this->optional_closing_tags[$parent_lower]) && isset($this->block_tags[$tag_lower]))
1314 {
1315 $this->parent->_[HDOM_INFO_END] = 0;
1316 $org_parent = $this->parent;
1317
1318 while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
1319 $this->parent = $this->parent->parent;
1320
1321 if (strtolower($this->parent->tag)!==$tag_lower) {
1322 $this->parent = $org_parent; // restore origonal parent
1323 if ($this->parent->parent) $this->parent = $this->parent->parent;
1324 $this->parent->_[HDOM_INFO_END] = $this->cursor;
1325 return $this->as_text_node($tag);
1326 }
1327 }
1328 else if (($this->parent->parent) && isset($this->block_tags[$tag_lower]))
1329 {
1330 $this->parent->_[HDOM_INFO_END] = 0;
1331 $org_parent = $this->parent;
1332
1333 while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
1334 $this->parent = $this->parent->parent;
1335
1336 if (strtolower($this->parent->tag)!==$tag_lower)
1337 {
1338 $this->parent = $org_parent; // restore origonal parent
1339 $this->parent->_[HDOM_INFO_END] = $this->cursor;
1340 return $this->as_text_node($tag);
1341 }
1342 }
1343 else if (($this->parent->parent) && strtolower($this->parent->parent->tag)===$tag_lower)
1344 {
1345 $this->parent->_[HDOM_INFO_END] = 0;
1346 $this->parent = $this->parent->parent;
1347 }
1348 else
1349 return $this->as_text_node($tag);
1350 }
1351
1352 $this->parent->_[HDOM_INFO_END] = $this->cursor;
1353 if ($this->parent->parent) $this->parent = $this->parent->parent;
1354
1355 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1356 return true;
1357 }
1358
1359 $node = new simple_html_dom_node($this);
1360 $node->_[HDOM_INFO_BEGIN] = $this->cursor;
1361 ++$this->cursor;
1362 $tag = $this->copy_until($this->token_slash);
1363 $node->tag_start = $begin_tag_pos;
1364
1365 // doctype, cdata & comments...
1366 if (isset($tag[0]) && $tag[0]==='!') {
1367 $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
1368
1369 if (isset($tag[2]) && $tag[1]==='-' && $tag[2]==='-') {
1370 $node->nodetype = HDOM_TYPE_COMMENT;
1371 $node->tag = 'comment';
1372 } else {
1373 $node->nodetype = HDOM_TYPE_UNKNOWN;
1374 $node->tag = 'unknown';
1375 }
1376 if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
1377 $this->link_nodes($node, true);
1378 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1379 return true;
1380 }
1381
1382 // text
1383 if ($pos=strpos($tag, '<')!==false) {
1384 $tag = '<' . substr($tag, 0, -1);
1385 $node->_[HDOM_INFO_TEXT] = $tag;
1386 $this->link_nodes($node, false);
1387 $this->char = $this->doc[--$this->pos]; // prev
1388 return true;
1389 }
1390
1391 // escaped hyphen here
1392 if (!preg_match("/^[\w\-:]+$/", $tag)) {
1393 $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
1394 if ($this->char==='<') {
1395 $this->link_nodes($node, false);
1396 return true;
1397 }
1398
1399 if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
1400 $this->link_nodes($node, false);
1401 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1402 return true;
1403 }
1404
1405 // begin tag
1406 $node->nodetype = HDOM_TYPE_ELEMENT;
1407 $tag_lower = strtolower($tag);
1408 $node->tag = ($this->lowercase) ? $tag_lower : $tag;
1409
1410 // handle optional closing tags
1411 if (isset($this->optional_closing_tags[$tag_lower]) )
1412 {
1413 while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)]))
1414 {
1415 $this->parent->_[HDOM_INFO_END] = 0;
1416 $this->parent = $this->parent->parent;
1417 }
1418 $node->parent = $this->parent;
1419 }
1420
1421 $guard = 0; // prevent infinity loop
1422 $space = array($this->copy_skip($this->token_blank), '', '');
1423
1424 // attributes
1425 do
1426 {
1427 if ($this->char!==null && $space[0]==='')
1428 {
1429 break;
1430 }
1431 $name = $this->copy_until($this->token_equal);
1432 if ($guard===$this->pos)
1433 {
1434 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1435 continue;
1436 }
1437 $guard = $this->pos;
1438
1439 // handle endless '<'
1440 if ($this->pos>=$this->size-1 && $this->char!=='>') {
1441 $node->nodetype = HDOM_TYPE_TEXT;
1442 $node->_[HDOM_INFO_END] = 0;
1443 $node->_[HDOM_INFO_TEXT] = '<'.$tag . $space[0] . $name;
1444 $node->tag = 'text';
1445 $this->link_nodes($node, false);
1446 return true;
1447 }
1448
1449 // handle mismatch '<'
1450 if ($this->doc[$this->pos-1]=='<') {
1451 $node->nodetype = HDOM_TYPE_TEXT;
1452 $node->tag = 'text';
1453 $node->attr = array();
1454 $node->_[HDOM_INFO_END] = 0;
1455 $node->_[HDOM_INFO_TEXT] = substr($this->doc, $begin_tag_pos, $this->pos-$begin_tag_pos-1);
1456 $this->pos -= 2;
1457 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1458 $this->link_nodes($node, false);
1459 return true;
1460 }
1461
1462 if ($name!=='/' && $name!=='') {
1463 $space[1] = $this->copy_skip($this->token_blank);
1464 $name = $this->restore_noise($name);
1465 if ($this->lowercase) $name = strtolower($name);
1466 if ($this->char==='=') {
1467 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1468 $this->parse_attr($node, $name, $space);
1469 }
1470 else {
1471 //no value attr: nowrap, checked selected...
1472 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
1473 $node->attr[$name] = true;
1474 if ($this->char!='>') $this->char = $this->doc[--$this->pos]; // prev
1475 }
1476 $node->_[HDOM_INFO_SPACE][] = $space;
1477 $space = array($this->copy_skip($this->token_blank), '', '');
1478 }
1479 else
1480 break;
1481 } while ($this->char!=='>' && $this->char!=='/');
1482
1483 $this->link_nodes($node, true);
1484 $node->_[HDOM_INFO_ENDSPACE] = $space[0];
1485
1486 // check self closing
1487 if ($this->copy_until_char_escape('>')==='/')
1488 {
1489 $node->_[HDOM_INFO_ENDSPACE] .= '/';
1490 $node->_[HDOM_INFO_END] = 0;
1491 }
1492 else
1493 {
1494 // reset parent
1495 if (!isset($this->self_closing_tags[strtolower($node->tag)])) $this->parent = $node;
1496 }
1497 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1498
1499 // If it's a BR tag, we need to set it's text to the default text.
1500 // This way when we see it in plaintext, we can generate formatting that the user wants.
1501 // since a br tag never has sub nodes, this works well.
1502 if ($node->tag == "br")
1503 {
1504 $node->_[HDOM_INFO_INNER] = $this->default_br_text;
1505 }
1506
1507 return true;
1508 }
1509
1510 // parse attributes
1511 protected function parse_attr($node, $name, &$space)
1512 {
1513 // Per sourceforge: http://sourceforge.net/tracker/?func=detail&aid=3061408&group_id=218559&atid=1044037
1514 // If the attribute is already defined inside a tag, only pay atetntion to the first one as opposed to the last one.
1515 if (isset($node->attr[$name]))
1516 {
1517 return;
1518 }
1519
1520 $space[2] = $this->copy_skip($this->token_blank);
1521 switch ($this->char) {
1522 case '"':
1523 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
1524 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1525 $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('"'));
1526 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1527 break;
1528 case '\'':
1529 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_SINGLE;
1530 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1531 $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('\''));
1532 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1533 break;
1534 default:
1535 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
1536 $node->attr[$name] = $this->restore_noise($this->copy_until($this->token_attr));
1537 }
1538 // PaperG: Attributes should not have \r or \n in them, that counts as html whitespace.
1539 $node->attr[$name] = str_replace("\r", "", $node->attr[$name]);
1540 $node->attr[$name] = str_replace("\n", "", $node->attr[$name]);
1541 // PaperG: If this is a "class" selector, lets get rid of the preceeding and trailing space since some people leave it in the multi class case.
1542 if ($name == "class") {
1543 $node->attr[$name] = trim($node->attr[$name]);
1544 }
1545 }
1546
1547 // link node's parent
1548 protected function link_nodes(&$node, $is_child)
1549 {
1550 $node->parent = $this->parent;
1551 $this->parent->nodes[] = $node;
1552 if ($is_child)
1553 {
1554 $this->parent->children[] = $node;
1555 }
1556 }
1557
1558 // as a text node
1559 protected function as_text_node($tag)
1560 {
1561 $node = new simple_html_dom_node($this);
1562 ++$this->cursor;
1563 $node->_[HDOM_INFO_TEXT] = '</' . $tag . '>';
1564 $this->link_nodes($node, false);
1565 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1566 return true;
1567 }
1568
1569 protected function skip($chars)
1570 {
1571 $this->pos += strspn($this->doc, $chars, $this->pos);
1572 $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1573 }
1574
1575 protected function copy_skip($chars)
1576 {
1577 $pos = $this->pos;
1578 $len = strspn($this->doc, $chars, $pos);
1579 $this->pos += $len;
1580 $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1581 if ($len===0) return '';
1582 return substr($this->doc, $pos, $len);
1583 }
1584
1585 protected function copy_until($chars)
1586 {
1587 $pos = $this->pos;
1588 $len = strcspn($this->doc, $chars, $pos);
1589 $this->pos += $len;
1590 $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1591 return substr($this->doc, $pos, $len);
1592 }
1593
1594 protected function copy_until_char($char)
1595 {
1596 if ($this->char===null) return '';
1597
1598 if (($pos = strpos($this->doc, $char, $this->pos))===false) {
1599 $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
1600 $this->char = null;
1601 $this->pos = $this->size;
1602 return $ret;
1603 }
1604
1605 if ($pos===$this->pos) return '';
1606 $pos_old = $this->pos;
1607 $this->char = $this->doc[$pos];
1608 $this->pos = $pos;
1609 return substr($this->doc, $pos_old, $pos-$pos_old);
1610 }
1611
1612 protected function copy_until_char_escape($char)
1613 {
1614 if ($this->char===null) return '';
1615
1616 $start = $this->pos;
1617 while (1)
1618 {
1619 if (($pos = strpos($this->doc, $char, $start))===false)
1620 {
1621 $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
1622 $this->char = null;
1623 $this->pos = $this->size;
1624 return $ret;
1625 }
1626
1627 if ($pos===$this->pos) return '';
1628
1629 if ($this->doc[$pos-1]==='\\') {
1630 $start = $pos+1;
1631 continue;
1632 }
1633
1634 $pos_old = $this->pos;
1635 $this->char = $this->doc[$pos];
1636 $this->pos = $pos;
1637 return substr($this->doc, $pos_old, $pos-$pos_old);
1638 }
1639 }
1640
1641 // remove noise from html content
1642 // save the noise in the $this->noise array.
1643 protected function remove_noise($pattern, $remove_tag=false)
1644 {
1645 global $debug_object;
1646 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1647
1648 $count = preg_match_all($pattern, $this->doc, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
1649
1650 for ($i=$count-1; $i>-1; --$i)
1651 {
1652 $key = '___noise___'.sprintf('% 5d', count($this->noise)+1000);
1653 if (is_object($debug_object)) { $debug_object->debug_log(2, 'key is: ' . $key); }
1654 $idx = ($remove_tag) ? 0 : 1;
1655 $this->noise[$key] = $matches[$i][$idx][0];
1656 $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
1657 }
1658
1659 // reset the length of content
1660 $this->size = strlen($this->doc);
1661 if ($this->size>0)
1662 {
1663 $this->char = $this->doc[0];
1664 }
1665 }
1666
1667 // restore noise to html content
1668 function restore_noise($text)
1669 {
1670 global $debug_object;
1671 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1672
1673 while (($pos=strpos($text, '___noise___'))!==false)
1674 {
1675 // Sometimes there is a broken piece of markup, and we don't GET the pos+11 etc... token which indicates a problem outside of us...
1676 if (strlen($text) > $pos+15)
1677 {
1678 $key = '___noise___'.$text[$pos+11].$text[$pos+12].$text[$pos+13].$text[$pos+14].$text[$pos+15];
1679 if (is_object($debug_object)) { $debug_object->debug_log(2, 'located key of: ' . $key); }
1680
1681 if (isset($this->noise[$key]))
1682 {
1683 $text = substr($text, 0, $pos).$this->noise[$key].substr($text, $pos+16);
1684 }
1685 else
1686 {
1687 // do this to prevent an infinite loop.
1688 $text = substr($text, 0, $pos).'UNDEFINED NOISE FOR KEY: '.$key . substr($text, $pos+16);
1689 }
1690 }
1691 else
1692 {
1693 // There is no valid key being given back to us... We must get rid of the ___noise___ or we will have a problem.
1694 $text = substr($text, 0, $pos).'NO NUMERIC NOISE KEY' . substr($text, $pos+11);
1695 }
1696 }
1697 return $text;
1698 }
1699
1700 // Sometimes we NEED one of the noise elements.
1701 function search_noise($text)
1702 {
1703 global $debug_object;
1704 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1705
1706 foreach($this->noise as $noiseElement)
1707 {
1708 if (strpos($noiseElement, $text)!==false)
1709 {
1710 return $noiseElement;
1711 }
1712 }
1713 }
1714 function __toString()
1715 {
1716 return $this->root->innertext();
1717 }
1718
1719 function __get($name)
1720 {
1721 switch ($name)
1722 {
1723 case 'outertext':
1724 return $this->root->innertext();
1725 case 'innertext':
1726 return $this->root->innertext();
1727 case 'plaintext':
1728 return $this->root->text();
1729 case 'charset':
1730 return $this->_charset;
1731 case 'target_charset':
1732 return $this->_target_charset;
1733 }
1734 }
1735
1736 // camel naming conventions
1737 function childNodes($idx=-1) {return $this->root->childNodes($idx);}
1738 function firstChild() {return $this->root->first_child();}
1739 function lastChild() {return $this->root->last_child();}
1740 function createElement($name, $value=null) {return @str_get_html("<$name>$value</$name>")->first_child();}
1741 function createTextNode($value) {return @end(str_get_html($value)->nodes);}
1742 function getElementById($id) {return $this->find("#$id", 0);}
1743 function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
1744 function getElementByTagName($name) {return $this->find($name, 0);}
1745 function getElementsByTagName($name, $idx=-1) {return $this->find($name, $idx);}
1746 function loadFile() {$args = func_get_args();$this->load_file($args);}
1747 }
1748
1749 ?>
1750