PluginProbe
MaxButtons – Create buttons / 6.3
MaxButtons – Create buttons v6.3
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 6.3, at assets/libraries/simplehtmldom/simple_html_dom.php

1,747 lines 53.6 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 $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
694 preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER);
695 if (is_object($debug_object)) {$debug_object->debug_log(2, "Matches Array: ", $matches);}
696
697 $selectors = array();
698 $result = array();
699
700
701 foreach ($matches as $m) {
702 $m[0] = trim($m[0]);
703 if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue;
704 // for browser generated xpath
705 if ($m[1]==='tbody') continue;
706
707 list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false);
708 if (!empty($m[2])) {$key='id'; $val=$m[2];}
709 if (!empty($m[3])) {$key='class'; $val=$m[3];}
710 if (!empty($m[4])) {$key=$m[4];}
711 if (!empty($m[5])) {$exp=$m[5];}
712 if (!empty($m[6])) {$val=$m[6];}
713
714 // convert to lowercase
715 if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);}
716 //elements that do NOT have the specified attribute
717 if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;}
718
719 $result[] = array($tag, $key, $val, $exp, $no_key);
720 if (trim($m[7])===',') {
721 $selectors[] = $result;
722 $result = array();
723 }
724 }
725 if (count($result)>0)
726 $selectors[] = $result;
727 return $selectors;
728 }
729
730 function __get($name)
731 {
732 if (isset($this->attr[$name]))
733 {
734 return $this->convert_text($this->attr[$name]);
735 }
736 switch ($name)
737 {
738 case 'outertext': return $this->outertext();
739 case 'innertext': return $this->innertext();
740 case 'plaintext': return $this->text();
741 case 'xmltext': return $this->xmltext();
742 default: return array_key_exists($name, $this->attr);
743 }
744 }
745
746 function __set($name, $value)
747 {
748 global $debug_object;
749 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
750
751 switch ($name)
752 {
753 case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
754 case 'innertext':
755 if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value;
756 return $this->_[HDOM_INFO_INNER] = $value;
757 }
758 if (!isset($this->attr[$name]))
759 {
760 $this->_[HDOM_INFO_SPACE][] = array(' ', '', '');
761 $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
762 }
763 $this->attr[$name] = $value;
764 }
765
766 function __isset($name)
767 {
768 switch ($name)
769 {
770 case 'outertext': return true;
771 case 'innertext': return true;
772 case 'plaintext': return true;
773 }
774 //no value attr: nowrap, checked selected...
775 return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]);
776 }
777
778 function __unset($name) {
779 if (isset($this->attr[$name]))
780 unset($this->attr[$name]);
781 }
782
783 // PaperG - Function to convert the text from one character set to another if the two sets are not the same.
784 function convert_text($text)
785 {
786 global $debug_object;
787 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
788
789 $converted_text = $text;
790
791 $sourceCharset = "";
792 $targetCharset = "";
793
794 if ($this->dom)
795 {
796 $sourceCharset = strtoupper($this->dom->_charset);
797 $targetCharset = strtoupper($this->dom->_target_charset);
798 }
799 if (is_object($debug_object)) {$debug_object->debug_log(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);}
800
801 if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0))
802 {
803 // Check if the reported encoding could have been incorrect and the text is actually already UTF-8
804 if ((strcasecmp($targetCharset, 'UTF-8') == 0) && ($this->is_utf8($text)))
805 {
806 $converted_text = $text;
807 }
808 else
809 {
810 $converted_text = iconv($sourceCharset, $targetCharset, $text);
811 }
812 }
813
814 // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output.
815 if ($targetCharset == 'UTF-8')
816 {
817 if (substr($converted_text, 0, 3) == "\xef\xbb\xbf")
818 {
819 $converted_text = substr($converted_text, 3);
820 }
821 if (substr($converted_text, -3) == "\xef\xbb\xbf")
822 {
823 $converted_text = substr($converted_text, 0, -3);
824 }
825 }
826
827 return $converted_text;
828 }
829
830 /**
831 * Returns true if $string is valid UTF-8 and false otherwise.
832 *
833 * @param mixed $str String to be tested
834 * @return boolean
835 */
836 static function is_utf8($str)
837 {
838 $c=0; $b=0;
839 $bits=0;
840 $len=strlen($str);
841 for($i=0; $i<$len; $i++)
842 {
843 $c=ord($str[$i]);
844 if($c > 128)
845 {
846 if(($c >= 254)) return false;
847 elseif($c >= 252) $bits=6;
848 elseif($c >= 248) $bits=5;
849 elseif($c >= 240) $bits=4;
850 elseif($c >= 224) $bits=3;
851 elseif($c >= 192) $bits=2;
852 else return false;
853 if(($i+$bits) > $len) return false;
854 while($bits > 1)
855 {
856 $i++;
857 $b=ord($str[$i]);
858 if($b < 128 || $b > 191) return false;
859 $bits--;
860 }
861 }
862 }
863 return true;
864 }
865 /*
866 function is_utf8($string)
867 {
868 //this is buggy
869 return (utf8_encode(utf8_decode($string)) == $string);
870 }
871 */
872
873 /**
874 * Function to try a few tricks to determine the displayed size of an img on the page.
875 * NOTE: This will ONLY work on an IMG tag. Returns FALSE on all other tag types.
876 *
877 * @author John Schlick
878 * @version April 19 2012
879 * @return array an array containing the 'height' and 'width' of the image on the page or -1 if we can't figure it out.
880 */
881 function get_display_size()
882 {
883 global $debug_object;
884
885 $width = -1;
886 $height = -1;
887
888 if ($this->tag !== 'img')
889 {
890 return false;
891 }
892
893 // See if there is aheight or width attribute in the tag itself.
894 if (isset($this->attr['width']))
895 {
896 $width = $this->attr['width'];
897 }
898
899 if (isset($this->attr['height']))
900 {
901 $height = $this->attr['height'];
902 }
903
904 // Now look for an inline style.
905 if (isset($this->attr['style']))
906 {
907 // Thanks to user gnarf from stackoverflow for this regular expression.
908 $attributes = array();
909 preg_match_all("/([\w-]+)\s*:\s*([^;]+)\s*;?/", $this->attr['style'], $matches, PREG_SET_ORDER);
910 foreach ($matches as $match) {
911 $attributes[$match[1]] = $match[2];
912 }
913
914 // If there is a width in the style attributes:
915 if (isset($attributes['width']) && $width == -1)
916 {
917 // check that the last two characters are px (pixels)
918 if (strtolower(substr($attributes['width'], -2)) == 'px')
919 {
920 $proposed_width = substr($attributes['width'], 0, -2);
921 // Now make sure that it's an integer and not something stupid.
922 if (filter_var($proposed_width, FILTER_VALIDATE_INT))
923 {
924 $width = $proposed_width;
925 }
926 }
927 }
928
929 // If there is a width in the style attributes:
930 if (isset($attributes['height']) && $height == -1)
931 {
932 // check that the last two characters are px (pixels)
933 if (strtolower(substr($attributes['height'], -2)) == 'px')
934 {
935 $proposed_height = substr($attributes['height'], 0, -2);
936 // Now make sure that it's an integer and not something stupid.
937 if (filter_var($proposed_height, FILTER_VALIDATE_INT))
938 {
939 $height = $proposed_height;
940 }
941 }
942 }
943
944 }
945
946 // Future enhancement:
947 // Look in the tag to see if there is a class or id specified that has a height or width attribute to it.
948
949 // Far future enhancement
950 // 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
951 // Note that in this case, the class or id will have the img subselector for it to apply to the image.
952
953 // ridiculously far future development
954 // 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.
955
956 $result = array('height' => $height,
957 'width' => $width);
958 return $result;
959 }
960
961 // camel naming conventions
962 function getAllAttributes() {return $this->attr;}
963 function getAttribute($name) {return $this->__get($name);}
964 function setAttribute($name, $value) {$this->__set($name, $value);}
965 function hasAttribute($name) {return $this->__isset($name);}
966 function removeAttribute($name) {$this->__set($name, null);}
967 function getElementById($id) {return $this->find("#$id", 0);}
968 function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
969 function getElementByTagName($name) {return $this->find($name, 0);}
970 function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);}
971 function parentNode() {return $this->parent();}
972 function childNodes($idx=-1) {return $this->children($idx);}
973 function firstChild() {return $this->first_child();}
974 function lastChild() {return $this->last_child();}
975 function nextSibling() {return $this->next_sibling();}
976 function previousSibling() {return $this->prev_sibling();}
977 function hasChildNodes() {return $this->has_child();}
978 function nodeName() {return $this->tag;}
979 function appendChild($node) {$node->parent($this); return $node;}
980
981 }
982
983 /**
984 * simple html dom parser
985 * Paperg - in the find routine: allow us to specify that we want case insensitive testing of the value of the selector.
986 * Paperg - change $size from protected to public so we can easily access it
987 * Paperg - added ForceTagsClosed in the constructor which tells us whether we trust the html or not. Default is to NOT trust it.
988 *
989 * @package PlaceLocalInclude
990 */
991 class simple_html_dom
992 {
993 public $root = null;
994 public $nodes = array();
995 public $callback = null;
996 public $lowercase = false;
997 // Used to keep track of how large the text was when we started.
998 public $original_size;
999 public $size;
1000 protected $pos;
1001 protected $doc;
1002 protected $char;
1003 protected $cursor;
1004 protected $parent;
1005 protected $noise = array();
1006 protected $token_blank = " \t\r\n";
1007 protected $token_equal = ' =/>';
1008 protected $token_slash = " />\r\n\t";
1009 protected $token_attr = ' >';
1010 // Note that this is referenced by a child node, and so it needs to be public for that node to see this information.
1011 public $_charset = '';
1012 public $_target_charset = '';
1013 protected $default_br_text = "";
1014 public $default_span_text = "";
1015
1016 // use isset instead of in_array, performance boost about 30%...
1017 protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1);
1018 protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1);
1019 // Known sourceforge issue #2977341
1020 // B tags that are not closed cause us to return everything to the end of the document.
1021 protected $optional_closing_tags = array(
1022 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1),
1023 'th'=>array('th'=>1),
1024 'td'=>array('td'=>1),
1025 'li'=>array('li'=>1),
1026 'dt'=>array('dt'=>1, 'dd'=>1),
1027 'dd'=>array('dd'=>1, 'dt'=>1),
1028 'dl'=>array('dd'=>1, 'dt'=>1),
1029 'p'=>array('p'=>1),
1030 'nobr'=>array('nobr'=>1),
1031 'b'=>array('b'=>1),
1032 'option'=>array('option'=>1),
1033 );
1034
1035 function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1036 {
1037 if ($str)
1038 {
1039 if (preg_match("/^http:\/\//i",$str) || is_file($str))
1040 {
1041 $this->load_file($str);
1042 }
1043 else
1044 {
1045 $this->load($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1046 }
1047 }
1048 // 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.
1049 if (!$forceTagsClosed) {
1050 $this->optional_closing_array=array();
1051 }
1052 $this->_target_charset = $target_charset;
1053 }
1054
1055 function __destruct()
1056 {
1057 $this->clear();
1058 }
1059
1060 // load html from string
1061 function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1062 {
1063 global $debug_object;
1064
1065 // prepare
1066 $this->prepare($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1067 // strip out cdata
1068 $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true);
1069 // strip out comments
1070 $this->remove_noise("'<!--(.*?)-->'is");
1071 // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037
1072 // Script tags removal now preceeds style tag removal.
1073 // strip out <script> tags
1074 $this->remove_noise("'<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>'is");
1075 $this->remove_noise("'<\s*script\s*>(.*?)<\s*/\s*script\s*>'is");
1076 // strip out <style> tags
1077 $this->remove_noise("'<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>'is");
1078 $this->remove_noise("'<\s*style\s*>(.*?)<\s*/\s*style\s*>'is");
1079 // strip out preformatted tags
1080 $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is");
1081 // strip out server side scripts
1082 $this->remove_noise("'(<\?)(.*?)(\?>)'s", true);
1083 // strip smarty scripts
1084 $this->remove_noise("'(\{\w)(.*?)(\})'s", true);
1085
1086 // parsing
1087 while ($this->parse());
1088 // end
1089 $this->root->_[HDOM_INFO_END] = $this->cursor;
1090 $this->parse_charset();
1091
1092 // make load function chainable
1093 return $this;
1094
1095 }
1096
1097 // load html from file
1098 function load_file()
1099 {
1100 $args = func_get_args();
1101 $this->load(call_user_func_array('file_get_contents', $args), true);
1102 // Throw an error if we can't properly load the dom.
1103 if (($error=error_get_last())!==null) {
1104 $this->clear();
1105 return false;
1106 }
1107 }
1108
1109 // set callback function
1110 function set_callback($function_name)
1111 {
1112 $this->callback = $function_name;
1113 }
1114
1115 // remove callback function
1116 function remove_callback()
1117 {
1118 $this->callback = null;
1119 }
1120
1121 // save dom as string
1122 function save($filepath='')
1123 {
1124 $ret = $this->root->innertext();
1125 if ($filepath!=='') file_put_contents($filepath, $ret, LOCK_EX);
1126 return $ret;
1127 }
1128
1129 // find dom node by css selector
1130 // Paperg - allow us to specify that we want case insensitive testing of the value of the selector.
1131 function find($selector, $idx=null, $lowercase=false)
1132 {
1133 return $this->root->find($selector, $idx, $lowercase);
1134 }
1135
1136 // clean up memory due to php5 circular references memory leak...
1137 function clear()
1138 {
1139 foreach ($this->nodes as $n) {$n->clear(); $n = null;}
1140 // 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.
1141 if (isset($this->children)) foreach ($this->children as $n) {$n->clear(); $n = null;}
1142 if (isset($this->parent)) {$this->parent->clear(); unset($this->parent);}
1143 if (isset($this->root)) {$this->root->clear(); unset($this->root);}
1144 unset($this->doc);
1145 unset($this->noise);
1146 }
1147
1148 function dump($show_attr=true)
1149 {
1150 $this->root->dump($show_attr);
1151 }
1152
1153 // prepare HTML data and init everything
1154 protected function prepare($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1155 {
1156 $this->clear();
1157
1158 // set the length of content before we do anything to it.
1159 $this->size = strlen($str);
1160 // Save the original size of the html that we got in. It might be useful to someone.
1161 $this->original_size = $this->size;
1162
1163 //before we save the string as the doc... strip out the \r \n's if we are told to.
1164 if ($stripRN) {
1165 $str = str_replace("\r", " ", $str);
1166 $str = str_replace("\n", " ", $str);
1167
1168 // set the length of content since we have changed it.
1169 $this->size = strlen($str);
1170 }
1171
1172 $this->doc = $str;
1173 $this->pos = 0;
1174 $this->cursor = 1;
1175 $this->noise = array();
1176 $this->nodes = array();
1177 $this->lowercase = $lowercase;
1178 $this->default_br_text = $defaultBRText;
1179 $this->default_span_text = $defaultSpanText;
1180 $this->root = new simple_html_dom_node($this);
1181 $this->root->tag = 'root';
1182 $this->root->_[HDOM_INFO_BEGIN] = -1;
1183 $this->root->nodetype = HDOM_TYPE_ROOT;
1184 $this->parent = $this->root;
1185 if ($this->size>0) $this->char = $this->doc[0];
1186 }
1187
1188 // parse html content
1189 protected function parse()
1190 {
1191 if (($s = $this->copy_until_char('<'))==='')
1192 {
1193 return $this->read_tag();
1194 }
1195
1196 // text
1197 $node = new simple_html_dom_node($this);
1198 ++$this->cursor;
1199 $node->_[HDOM_INFO_TEXT] = $s;
1200 $this->link_nodes($node, false);
1201 return true;
1202 }
1203
1204 // 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.
1205 // 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
1206 // (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.
1207 protected function parse_charset()
1208 {
1209 global $debug_object;
1210
1211 $charset = null;
1212
1213 if (function_exists('get_last_retrieve_url_contents_content_type'))
1214 {
1215 $contentTypeHeader = get_last_retrieve_url_contents_content_type();
1216 $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches);
1217 if ($success)
1218 {
1219 $charset = $matches[1];
1220 if (is_object($debug_object)) {$debug_object->debug_log(2, 'header content-type found charset of: ' . $charset);}
1221 }
1222
1223 }
1224
1225 if (empty($charset))
1226 {
1227 $el = $this->root->find('meta[http-equiv=Content-Type]',0, true);
1228 if (!empty($el))
1229 {
1230 $fullvalue = $el->content;
1231 if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag found' . $fullvalue);}
1232
1233 if (!empty($fullvalue))
1234 {
1235 $success = preg_match('/charset=(.+)/i', $fullvalue, $matches);
1236 if ($success)
1237 {
1238 $charset = $matches[1];
1239 }
1240 else
1241 {
1242 // If there is a meta tag, and they don't specify the character set, research says that it's typically ISO-8859-1
1243 if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag couldn\'t be parsed. using iso-8859 default.');}
1244 $charset = 'ISO-8859-1';
1245 }
1246 }
1247 }
1248 }
1249
1250 // If we couldn't find a charset above, then lets try to detect one based on the text we got...
1251 if (empty($charset))
1252 {
1253 // Use this in case mb_detect_charset isn't installed/loaded on this machine.
1254 $charset = false;
1255 if (function_exists('mb_detect_encoding'))
1256 {
1257 // Have php try to detect the encoding from the text given to us.
1258 $charset = mb_detect_encoding($this->root->plaintext . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) );
1259 if (is_object($debug_object)) {$debug_object->debug_log(2, 'mb_detect found: ' . $charset);}
1260 }
1261
1262 // 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...
1263 if ($charset === false)
1264 {
1265 if (is_object($debug_object)) {$debug_object->debug_log(2, 'since mb_detect failed - using default of utf-8');}
1266 $charset = 'UTF-8';
1267 }
1268 }
1269
1270 // Since CP1252 is a superset, if we get one of it's subsets, we want it instead.
1271 if ((strtolower($charset) == strtolower('ISO-8859-1')) || (strtolower($charset) == strtolower('Latin1')) || (strtolower($charset) == strtolower('Latin-1')))
1272 {
1273 if (is_object($debug_object)) {$debug_object->debug_log(2, 'replacing ' . $charset . ' with CP1252 as its a superset');}
1274 $charset = 'CP1252';
1275 }
1276
1277 if (is_object($debug_object)) {$debug_object->debug_log(1, 'EXIT - ' . $charset);}
1278
1279 return $this->_charset = $charset;
1280 }
1281
1282 // read tag info
1283 protected function read_tag()
1284 {
1285 if ($this->char!=='<')
1286 {
1287 $this->root->_[HDOM_INFO_END] = $this->cursor;
1288 return false;
1289 }
1290 $begin_tag_pos = $this->pos;
1291 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1292
1293 // end tag
1294 if ($this->char==='/')
1295 {
1296 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1297 // This represents the change in the simple_html_dom trunk from revision 180 to 181.
1298 // $this->skip($this->token_blank_t);
1299 $this->skip($this->token_blank);
1300 $tag = $this->copy_until_char('>');
1301
1302 // skip attributes in end tag
1303 if (($pos = strpos($tag, ' '))!==false)
1304 $tag = substr($tag, 0, $pos);
1305
1306 $parent_lower = strtolower($this->parent->tag);
1307 $tag_lower = strtolower($tag);
1308
1309 if ($parent_lower!==$tag_lower)
1310 {
1311 if (isset($this->optional_closing_tags[$parent_lower]) && isset($this->block_tags[$tag_lower]))
1312 {
1313 $this->parent->_[HDOM_INFO_END] = 0;
1314 $org_parent = $this->parent;
1315
1316 while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
1317 $this->parent = $this->parent->parent;
1318
1319 if (strtolower($this->parent->tag)!==$tag_lower) {
1320 $this->parent = $org_parent; // restore origonal parent
1321 if ($this->parent->parent) $this->parent = $this->parent->parent;
1322 $this->parent->_[HDOM_INFO_END] = $this->cursor;
1323 return $this->as_text_node($tag);
1324 }
1325 }
1326 else if (($this->parent->parent) && isset($this->block_tags[$tag_lower]))
1327 {
1328 $this->parent->_[HDOM_INFO_END] = 0;
1329 $org_parent = $this->parent;
1330
1331 while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
1332 $this->parent = $this->parent->parent;
1333
1334 if (strtolower($this->parent->tag)!==$tag_lower)
1335 {
1336 $this->parent = $org_parent; // restore origonal parent
1337 $this->parent->_[HDOM_INFO_END] = $this->cursor;
1338 return $this->as_text_node($tag);
1339 }
1340 }
1341 else if (($this->parent->parent) && strtolower($this->parent->parent->tag)===$tag_lower)
1342 {
1343 $this->parent->_[HDOM_INFO_END] = 0;
1344 $this->parent = $this->parent->parent;
1345 }
1346 else
1347 return $this->as_text_node($tag);
1348 }
1349
1350 $this->parent->_[HDOM_INFO_END] = $this->cursor;
1351 if ($this->parent->parent) $this->parent = $this->parent->parent;
1352
1353 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1354 return true;
1355 }
1356
1357 $node = new simple_html_dom_node($this);
1358 $node->_[HDOM_INFO_BEGIN] = $this->cursor;
1359 ++$this->cursor;
1360 $tag = $this->copy_until($this->token_slash);
1361 $node->tag_start = $begin_tag_pos;
1362
1363 // doctype, cdata & comments...
1364 if (isset($tag[0]) && $tag[0]==='!') {
1365 $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
1366
1367 if (isset($tag[2]) && $tag[1]==='-' && $tag[2]==='-') {
1368 $node->nodetype = HDOM_TYPE_COMMENT;
1369 $node->tag = 'comment';
1370 } else {
1371 $node->nodetype = HDOM_TYPE_UNKNOWN;
1372 $node->tag = 'unknown';
1373 }
1374 if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
1375 $this->link_nodes($node, true);
1376 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1377 return true;
1378 }
1379
1380 // text
1381 if ($pos=strpos($tag, '<')!==false) {
1382 $tag = '<' . substr($tag, 0, -1);
1383 $node->_[HDOM_INFO_TEXT] = $tag;
1384 $this->link_nodes($node, false);
1385 $this->char = $this->doc[--$this->pos]; // prev
1386 return true;
1387 }
1388
1389 if (!preg_match("/^[\w-:]+$/", $tag)) {
1390 $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
1391 if ($this->char==='<') {
1392 $this->link_nodes($node, false);
1393 return true;
1394 }
1395
1396 if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
1397 $this->link_nodes($node, false);
1398 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1399 return true;
1400 }
1401
1402 // begin tag
1403 $node->nodetype = HDOM_TYPE_ELEMENT;
1404 $tag_lower = strtolower($tag);
1405 $node->tag = ($this->lowercase) ? $tag_lower : $tag;
1406
1407 // handle optional closing tags
1408 if (isset($this->optional_closing_tags[$tag_lower]) )
1409 {
1410 while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)]))
1411 {
1412 $this->parent->_[HDOM_INFO_END] = 0;
1413 $this->parent = $this->parent->parent;
1414 }
1415 $node->parent = $this->parent;
1416 }
1417
1418 $guard = 0; // prevent infinity loop
1419 $space = array($this->copy_skip($this->token_blank), '', '');
1420
1421 // attributes
1422 do
1423 {
1424 if ($this->char!==null && $space[0]==='')
1425 {
1426 break;
1427 }
1428 $name = $this->copy_until($this->token_equal);
1429 if ($guard===$this->pos)
1430 {
1431 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1432 continue;
1433 }
1434 $guard = $this->pos;
1435
1436 // handle endless '<'
1437 if ($this->pos>=$this->size-1 && $this->char!=='>') {
1438 $node->nodetype = HDOM_TYPE_TEXT;
1439 $node->_[HDOM_INFO_END] = 0;
1440 $node->_[HDOM_INFO_TEXT] = '<'.$tag . $space[0] . $name;
1441 $node->tag = 'text';
1442 $this->link_nodes($node, false);
1443 return true;
1444 }
1445
1446 // handle mismatch '<'
1447 if ($this->doc[$this->pos-1]=='<') {
1448 $node->nodetype = HDOM_TYPE_TEXT;
1449 $node->tag = 'text';
1450 $node->attr = array();
1451 $node->_[HDOM_INFO_END] = 0;
1452 $node->_[HDOM_INFO_TEXT] = substr($this->doc, $begin_tag_pos, $this->pos-$begin_tag_pos-1);
1453 $this->pos -= 2;
1454 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1455 $this->link_nodes($node, false);
1456 return true;
1457 }
1458
1459 if ($name!=='/' && $name!=='') {
1460 $space[1] = $this->copy_skip($this->token_blank);
1461 $name = $this->restore_noise($name);
1462 if ($this->lowercase) $name = strtolower($name);
1463 if ($this->char==='=') {
1464 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1465 $this->parse_attr($node, $name, $space);
1466 }
1467 else {
1468 //no value attr: nowrap, checked selected...
1469 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
1470 $node->attr[$name] = true;
1471 if ($this->char!='>') $this->char = $this->doc[--$this->pos]; // prev
1472 }
1473 $node->_[HDOM_INFO_SPACE][] = $space;
1474 $space = array($this->copy_skip($this->token_blank), '', '');
1475 }
1476 else
1477 break;
1478 } while ($this->char!=='>' && $this->char!=='/');
1479
1480 $this->link_nodes($node, true);
1481 $node->_[HDOM_INFO_ENDSPACE] = $space[0];
1482
1483 // check self closing
1484 if ($this->copy_until_char_escape('>')==='/')
1485 {
1486 $node->_[HDOM_INFO_ENDSPACE] .= '/';
1487 $node->_[HDOM_INFO_END] = 0;
1488 }
1489 else
1490 {
1491 // reset parent
1492 if (!isset($this->self_closing_tags[strtolower($node->tag)])) $this->parent = $node;
1493 }
1494 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1495
1496 // If it's a BR tag, we need to set it's text to the default text.
1497 // This way when we see it in plaintext, we can generate formatting that the user wants.
1498 // since a br tag never has sub nodes, this works well.
1499 if ($node->tag == "br")
1500 {
1501 $node->_[HDOM_INFO_INNER] = $this->default_br_text;
1502 }
1503
1504 return true;
1505 }
1506
1507 // parse attributes
1508 protected function parse_attr($node, $name, &$space)
1509 {
1510 // Per sourceforge: http://sourceforge.net/tracker/?func=detail&aid=3061408&group_id=218559&atid=1044037
1511 // If the attribute is already defined inside a tag, only pay atetntion to the first one as opposed to the last one.
1512 if (isset($node->attr[$name]))
1513 {
1514 return;
1515 }
1516
1517 $space[2] = $this->copy_skip($this->token_blank);
1518 switch ($this->char) {
1519 case '"':
1520 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
1521 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1522 $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('"'));
1523 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1524 break;
1525 case '\'':
1526 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_SINGLE;
1527 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1528 $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('\''));
1529 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1530 break;
1531 default:
1532 $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
1533 $node->attr[$name] = $this->restore_noise($this->copy_until($this->token_attr));
1534 }
1535 // PaperG: Attributes should not have \r or \n in them, that counts as html whitespace.
1536 $node->attr[$name] = str_replace("\r", "", $node->attr[$name]);
1537 $node->attr[$name] = str_replace("\n", "", $node->attr[$name]);
1538 // 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.
1539 if ($name == "class") {
1540 $node->attr[$name] = trim($node->attr[$name]);
1541 }
1542 }
1543
1544 // link node's parent
1545 protected function link_nodes(&$node, $is_child)
1546 {
1547 $node->parent = $this->parent;
1548 $this->parent->nodes[] = $node;
1549 if ($is_child)
1550 {
1551 $this->parent->children[] = $node;
1552 }
1553 }
1554
1555 // as a text node
1556 protected function as_text_node($tag)
1557 {
1558 $node = new simple_html_dom_node($this);
1559 ++$this->cursor;
1560 $node->_[HDOM_INFO_TEXT] = '</' . $tag . '>';
1561 $this->link_nodes($node, false);
1562 $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1563 return true;
1564 }
1565
1566 protected function skip($chars)
1567 {
1568 $this->pos += strspn($this->doc, $chars, $this->pos);
1569 $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1570 }
1571
1572 protected function copy_skip($chars)
1573 {
1574 $pos = $this->pos;
1575 $len = strspn($this->doc, $chars, $pos);
1576 $this->pos += $len;
1577 $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1578 if ($len===0) return '';
1579 return substr($this->doc, $pos, $len);
1580 }
1581
1582 protected function copy_until($chars)
1583 {
1584 $pos = $this->pos;
1585 $len = strcspn($this->doc, $chars, $pos);
1586 $this->pos += $len;
1587 $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1588 return substr($this->doc, $pos, $len);
1589 }
1590
1591 protected function copy_until_char($char)
1592 {
1593 if ($this->char===null) return '';
1594
1595 if (($pos = strpos($this->doc, $char, $this->pos))===false) {
1596 $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
1597 $this->char = null;
1598 $this->pos = $this->size;
1599 return $ret;
1600 }
1601
1602 if ($pos===$this->pos) return '';
1603 $pos_old = $this->pos;
1604 $this->char = $this->doc[$pos];
1605 $this->pos = $pos;
1606 return substr($this->doc, $pos_old, $pos-$pos_old);
1607 }
1608
1609 protected function copy_until_char_escape($char)
1610 {
1611 if ($this->char===null) return '';
1612
1613 $start = $this->pos;
1614 while (1)
1615 {
1616 if (($pos = strpos($this->doc, $char, $start))===false)
1617 {
1618 $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
1619 $this->char = null;
1620 $this->pos = $this->size;
1621 return $ret;
1622 }
1623
1624 if ($pos===$this->pos) return '';
1625
1626 if ($this->doc[$pos-1]==='\\') {
1627 $start = $pos+1;
1628 continue;
1629 }
1630
1631 $pos_old = $this->pos;
1632 $this->char = $this->doc[$pos];
1633 $this->pos = $pos;
1634 return substr($this->doc, $pos_old, $pos-$pos_old);
1635 }
1636 }
1637
1638 // remove noise from html content
1639 // save the noise in the $this->noise array.
1640 protected function remove_noise($pattern, $remove_tag=false)
1641 {
1642 global $debug_object;
1643 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1644
1645 $count = preg_match_all($pattern, $this->doc, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
1646
1647 for ($i=$count-1; $i>-1; --$i)
1648 {
1649 $key = '___noise___'.sprintf('% 5d', count($this->noise)+1000);
1650 if (is_object($debug_object)) { $debug_object->debug_log(2, 'key is: ' . $key); }
1651 $idx = ($remove_tag) ? 0 : 1;
1652 $this->noise[$key] = $matches[$i][$idx][0];
1653 $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
1654 }
1655
1656 // reset the length of content
1657 $this->size = strlen($this->doc);
1658 if ($this->size>0)
1659 {
1660 $this->char = $this->doc[0];
1661 }
1662 }
1663
1664 // restore noise to html content
1665 function restore_noise($text)
1666 {
1667 global $debug_object;
1668 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1669
1670 while (($pos=strpos($text, '___noise___'))!==false)
1671 {
1672 // 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...
1673 if (strlen($text) > $pos+15)
1674 {
1675 $key = '___noise___'.$text[$pos+11].$text[$pos+12].$text[$pos+13].$text[$pos+14].$text[$pos+15];
1676 if (is_object($debug_object)) { $debug_object->debug_log(2, 'located key of: ' . $key); }
1677
1678 if (isset($this->noise[$key]))
1679 {
1680 $text = substr($text, 0, $pos).$this->noise[$key].substr($text, $pos+16);
1681 }
1682 else
1683 {
1684 // do this to prevent an infinite loop.
1685 $text = substr($text, 0, $pos).'UNDEFINED NOISE FOR KEY: '.$key . substr($text, $pos+16);
1686 }
1687 }
1688 else
1689 {
1690 // There is no valid key being given back to us... We must get rid of the ___noise___ or we will have a problem.
1691 $text = substr($text, 0, $pos).'NO NUMERIC NOISE KEY' . substr($text, $pos+11);
1692 }
1693 }
1694 return $text;
1695 }
1696
1697 // Sometimes we NEED one of the noise elements.
1698 function search_noise($text)
1699 {
1700 global $debug_object;
1701 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1702
1703 foreach($this->noise as $noiseElement)
1704 {
1705 if (strpos($noiseElement, $text)!==false)
1706 {
1707 return $noiseElement;
1708 }
1709 }
1710 }
1711 function __toString()
1712 {
1713 return $this->root->innertext();
1714 }
1715
1716 function __get($name)
1717 {
1718 switch ($name)
1719 {
1720 case 'outertext':
1721 return $this->root->innertext();
1722 case 'innertext':
1723 return $this->root->innertext();
1724 case 'plaintext':
1725 return $this->root->text();
1726 case 'charset':
1727 return $this->_charset;
1728 case 'target_charset':
1729 return $this->_target_charset;
1730 }
1731 }
1732
1733 // camel naming conventions
1734 function childNodes($idx=-1) {return $this->root->childNodes($idx);}
1735 function firstChild() {return $this->root->first_child();}
1736 function lastChild() {return $this->root->last_child();}
1737 function createElement($name, $value=null) {return @str_get_html("<$name>$value</$name>")->first_child();}
1738 function createTextNode($value) {return @end(str_get_html($value)->nodes);}
1739 function getElementById($id) {return $this->find("#$id", 0);}
1740 function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
1741 function getElementByTagName($name) {return $this->find($name, 0);}
1742 function getElementsByTagName($name, $idx=-1) {return $this->find($name, $idx);}
1743 function loadFile() {$args = func_get_args();$this->load_file($args);}
1744 }
1745
1746 ?>
1747