Version 1
[yaffs-website] / vendor / phpunit / phpunit / src / Util / XML.php
1 <?php
2 /*
3  * This file is part of PHPUnit.
4  *
5  * (c) Sebastian Bergmann <sebastian@phpunit.de>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * XML helpers.
13  *
14  * @since Class available since Release 3.2.0
15  */
16 class PHPUnit_Util_XML
17 {
18     /**
19      * Escapes a string for the use in XML documents
20      * Any Unicode character is allowed, excluding the surrogate blocks, FFFE,
21      * and FFFF (not even as character reference).
22      * See http://www.w3.org/TR/xml/#charsets
23      *
24      * @param string $string
25      *
26      * @return string
27      *
28      * @since  Method available since Release 3.4.6
29      */
30     public static function prepareString($string)
31     {
32         return preg_replace(
33             '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/',
34             '',
35             htmlspecialchars(
36                 PHPUnit_Util_String::convertToUtf8($string),
37                 ENT_QUOTES,
38                 'UTF-8'
39             )
40         );
41     }
42
43     /**
44      * Loads an XML (or HTML) file into a DOMDocument object.
45      *
46      * @param string $filename
47      * @param bool   $isHtml
48      * @param bool   $xinclude
49      * @param bool   $strict
50      *
51      * @return DOMDocument
52      *
53      * @since  Method available since Release 3.3.0
54      */
55     public static function loadFile($filename, $isHtml = false, $xinclude = false, $strict = false)
56     {
57         $reporting = error_reporting(0);
58         $contents  = file_get_contents($filename);
59         error_reporting($reporting);
60
61         if ($contents === false) {
62             throw new PHPUnit_Framework_Exception(
63                 sprintf(
64                     'Could not read "%s".',
65                     $filename
66                 )
67             );
68         }
69
70         return self::load($contents, $isHtml, $filename, $xinclude, $strict);
71     }
72
73     /**
74      * Load an $actual document into a DOMDocument.  This is called
75      * from the selector assertions.
76      *
77      * If $actual is already a DOMDocument, it is returned with
78      * no changes.  Otherwise, $actual is loaded into a new DOMDocument
79      * as either HTML or XML, depending on the value of $isHtml. If $isHtml is
80      * false and $xinclude is true, xinclude is performed on the loaded
81      * DOMDocument.
82      *
83      * Note: prior to PHPUnit 3.3.0, this method loaded a file and
84      * not a string as it currently does.  To load a file into a
85      * DOMDocument, use loadFile() instead.
86      *
87      * @param string|DOMDocument $actual
88      * @param bool               $isHtml
89      * @param string             $filename
90      * @param bool               $xinclude
91      * @param bool               $strict
92      *
93      * @return DOMDocument
94      *
95      * @since  Method available since Release 3.3.0
96      */
97     public static function load($actual, $isHtml = false, $filename = '', $xinclude = false, $strict = false)
98     {
99         if ($actual instanceof DOMDocument) {
100             return $actual;
101         }
102
103         if (!is_string($actual)) {
104             throw new PHPUnit_Framework_Exception('Could not load XML from ' . gettype($actual));
105         }
106
107         if ($actual === '') {
108             throw new PHPUnit_Framework_Exception('Could not load XML from empty string');
109         }
110
111         // Required for XInclude on Windows.
112         if ($xinclude) {
113             $cwd = getcwd();
114             @chdir(dirname($filename));
115         }
116
117         $document                     = new DOMDocument;
118         $document->preserveWhiteSpace = false;
119
120         $internal  = libxml_use_internal_errors(true);
121         $message   = '';
122         $reporting = error_reporting(0);
123
124         if ('' !== $filename) {
125             // Necessary for xinclude
126             $document->documentURI = $filename;
127         }
128
129         if ($isHtml) {
130             $loaded = $document->loadHTML($actual);
131         } else {
132             $loaded = $document->loadXML($actual);
133         }
134
135         if (!$isHtml && $xinclude) {
136             $document->xinclude();
137         }
138
139         foreach (libxml_get_errors() as $error) {
140             $message .= "\n" . $error->message;
141         }
142
143         libxml_use_internal_errors($internal);
144         error_reporting($reporting);
145
146         if ($xinclude) {
147             @chdir($cwd);
148         }
149
150         if ($loaded === false || ($strict && $message !== '')) {
151             if ($filename !== '') {
152                 throw new PHPUnit_Framework_Exception(
153                     sprintf(
154                         'Could not load "%s".%s',
155                         $filename,
156                         $message != '' ? "\n" . $message : ''
157                     )
158                 );
159             } else {
160                 if ($message === '') {
161                     $message = 'Could not load XML for unknown reason';
162                 }
163                 throw new PHPUnit_Framework_Exception($message);
164             }
165         }
166
167         return $document;
168     }
169
170     /**
171      * @param DOMNode $node
172      *
173      * @return string
174      *
175      * @since  Method available since Release 3.4.0
176      */
177     public static function nodeToText(DOMNode $node)
178     {
179         if ($node->childNodes->length == 1) {
180             return $node->textContent;
181         }
182
183         $result = '';
184
185         foreach ($node->childNodes as $childNode) {
186             $result .= $node->ownerDocument->saveXML($childNode);
187         }
188
189         return $result;
190     }
191
192     /**
193      * @param DOMNode $node
194      *
195      * @since  Method available since Release 3.3.0
196      */
197     public static function removeCharacterDataNodes(DOMNode $node)
198     {
199         if ($node->hasChildNodes()) {
200             for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
201                 if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
202                     $node->removeChild($child);
203                 }
204             }
205         }
206     }
207
208     /**
209      * "Convert" a DOMElement object into a PHP variable.
210      *
211      * @param DOMElement $element
212      *
213      * @return mixed
214      *
215      * @since  Method available since Release 3.4.0
216      */
217     public static function xmlToVariable(DOMElement $element)
218     {
219         $variable = null;
220
221         switch ($element->tagName) {
222             case 'array':
223                 $variable = array();
224
225                 foreach ($element->childNodes as $entry) {
226                     if (!$entry instanceof DOMElement || $entry->tagName !== 'element') {
227                         continue;
228                     }
229                     $item = $entry->childNodes->item(0);
230
231                     if ($item instanceof DOMText) {
232                         $item = $entry->childNodes->item(1);
233                     }
234
235                     $value = self::xmlToVariable($item);
236
237                     if ($entry->hasAttribute('key')) {
238                         $variable[(string) $entry->getAttribute('key')] = $value;
239                     } else {
240                         $variable[] = $value;
241                     }
242                 }
243                 break;
244
245             case 'object':
246                 $className = $element->getAttribute('class');
247
248                 if ($element->hasChildNodes()) {
249                     $arguments       = $element->childNodes->item(1)->childNodes;
250                     $constructorArgs = array();
251
252                     foreach ($arguments as $argument) {
253                         if ($argument instanceof DOMElement) {
254                             $constructorArgs[] = self::xmlToVariable($argument);
255                         }
256                     }
257
258                     $class    = new ReflectionClass($className);
259                     $variable = $class->newInstanceArgs($constructorArgs);
260                 } else {
261                     $variable = new $className;
262                 }
263                 break;
264
265             case 'boolean':
266                 $variable = $element->textContent == 'true' ? true : false;
267                 break;
268
269             case 'integer':
270             case 'double':
271             case 'string':
272                 $variable = $element->textContent;
273
274                 settype($variable, $element->tagName);
275                 break;
276         }
277
278         return $variable;
279     }
280
281     /**
282      * Validate list of keys in the associative array.
283      *
284      * @param array $hash
285      * @param array $validKeys
286      *
287      * @return array
288      *
289      * @throws PHPUnit_Framework_Exception
290      *
291      * @since  Method available since Release 3.3.0
292      */
293     public static function assertValidKeys(array $hash, array $validKeys)
294     {
295         $valids = array();
296
297         // Normalize validation keys so that we can use both indexed and
298         // associative arrays.
299         foreach ($validKeys as $key => $val) {
300             is_int($key) ? $valids[$val] = null : $valids[$key] = $val;
301         }
302
303         $validKeys = array_keys($valids);
304
305         // Check for invalid keys.
306         foreach ($hash as $key => $value) {
307             if (!in_array($key, $validKeys)) {
308                 $unknown[] = $key;
309             }
310         }
311
312         if (!empty($unknown)) {
313             throw new PHPUnit_Framework_Exception(
314                 'Unknown key(s): ' . implode(', ', $unknown)
315             );
316         }
317
318         // Add default values for any valid keys that are empty.
319         foreach ($valids as $key => $value) {
320             if (!isset($hash[$key])) {
321                 $hash[$key] = $value;
322             }
323         }
324
325         return $hash;
326     }
327
328     /**
329      * Parse a CSS selector into an associative array suitable for
330      * use with findNodes().
331      *
332      * @param string $selector
333      * @param mixed  $content
334      *
335      * @return array
336      *
337      * @since  Method available since Release 3.3.0
338      */
339     public static function convertSelectToTag($selector, $content = true)
340     {
341         $selector = trim(preg_replace("/\s+/", ' ', $selector));
342
343         // substitute spaces within attribute value
344         while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
345             $selector = preg_replace(
346                 '/(\[[^\]]+"[^"]+)\s([^"]+"\])/',
347                 '$1__SPACE__$2',
348                 $selector
349             );
350         }
351
352         if (strstr($selector, ' ')) {
353             $elements = explode(' ', $selector);
354         } else {
355             $elements = array($selector);
356         }
357
358         $previousTag = array();
359
360         foreach (array_reverse($elements) as $element) {
361             $element = str_replace('__SPACE__', ' ', $element);
362
363             // child selector
364             if ($element == '>') {
365                 $previousTag = array('child' => $previousTag['descendant']);
366                 continue;
367             }
368
369             // adjacent-sibling selector
370             if ($element == '+') {
371                 $previousTag = array('adjacent-sibling' => $previousTag['descendant']);
372                 continue;
373             }
374
375             $tag = array();
376
377             // match element tag
378             preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
379
380             if (!empty($eltMatches[1])) {
381                 $tag['tag'] = $eltMatches[1];
382             }
383
384             // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*),
385             // and classes (\.[^\.#\[]*))
386             preg_match_all(
387                 "/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/",
388                 $element,
389                 $matches
390             );
391
392             if (!empty($matches[1])) {
393                 $classes = array();
394                 $attrs   = array();
395
396                 foreach ($matches[1] as $match) {
397                     // id matched
398                     if (substr($match, 0, 1) == '#') {
399                         $tag['id'] = substr($match, 1);
400                     } // class matched
401                     elseif (substr($match, 0, 1) == '.') {
402                         $classes[] = substr($match, 1);
403                     } // attribute matched
404                     elseif (substr($match, 0, 1) == '[' &&
405                              substr($match, -1, 1) == ']') {
406                         $attribute = substr($match, 1, strlen($match) - 2);
407                         $attribute = str_replace('"', '', $attribute);
408
409                         // match single word
410                         if (strstr($attribute, '~=')) {
411                             list($key, $value) = explode('~=', $attribute);
412                             $value             = "regexp:/.*\b$value\b.*/";
413                         } // match substring
414                         elseif (strstr($attribute, '*=')) {
415                             list($key, $value) = explode('*=', $attribute);
416                             $value             = "regexp:/.*$value.*/";
417                         } // exact match
418                         else {
419                             list($key, $value) = explode('=', $attribute);
420                         }
421
422                         $attrs[$key] = $value;
423                     }
424                 }
425
426                 if (!empty($classes)) {
427                     $tag['class'] = implode(' ', $classes);
428                 }
429
430                 if (!empty($attrs)) {
431                     $tag['attributes'] = $attrs;
432                 }
433             }
434
435             // tag content
436             if (is_string($content)) {
437                 $tag['content'] = $content;
438             }
439
440             // determine previous child/descendants
441             if (!empty($previousTag['descendant'])) {
442                 $tag['descendant'] = $previousTag['descendant'];
443             } elseif (!empty($previousTag['child'])) {
444                 $tag['child'] = $previousTag['child'];
445             } elseif (!empty($previousTag['adjacent-sibling'])) {
446                 $tag['adjacent-sibling'] = $previousTag['adjacent-sibling'];
447                 unset($tag['content']);
448             }
449
450             $previousTag = array('descendant' => $tag);
451         }
452
453         return $tag;
454     }
455
456     /**
457      * Parse an $actual document and return an array of DOMNodes
458      * matching the CSS $selector.  If an error occurs, it will
459      * return false.
460      *
461      * To only return nodes containing a certain content, give
462      * the $content to match as a string.  Otherwise, setting
463      * $content to true will return all nodes matching $selector.
464      *
465      * The $actual document may be a DOMDocument or a string
466      * containing XML or HTML, identified by $isHtml.
467      *
468      * @param array  $selector
469      * @param string $content
470      * @param mixed  $actual
471      * @param bool   $isHtml
472      *
473      * @return bool|array
474      *
475      * @since  Method available since Release 3.3.0
476      */
477     public static function cssSelect($selector, $content, $actual, $isHtml = true)
478     {
479         $matcher = self::convertSelectToTag($selector, $content);
480         $dom     = self::load($actual, $isHtml);
481         $tags    = self::findNodes($dom, $matcher, $isHtml);
482
483         return $tags;
484     }
485
486     /**
487      * Parse out the options from the tag using DOM object tree.
488      *
489      * @param DOMDocument $dom
490      * @param array       $options
491      * @param bool        $isHtml
492      *
493      * @return array
494      *
495      * @since  Method available since Release 3.3.0
496      */
497     public static function findNodes(DOMDocument $dom, array $options, $isHtml = true)
498     {
499         $valid = array(
500           'id', 'class', 'tag', 'content', 'attributes', 'parent',
501           'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
502         );
503
504         $filtered = array();
505         $options  = self::assertValidKeys($options, $valid);
506
507         // find the element by id
508         if ($options['id']) {
509             $options['attributes']['id'] = $options['id'];
510         }
511
512         if ($options['class']) {
513             $options['attributes']['class'] = $options['class'];
514         }
515
516         $nodes = array();
517
518         // find the element by a tag type
519         if ($options['tag']) {
520             if ($isHtml) {
521                 $elements = self::getElementsByCaseInsensitiveTagName(
522                     $dom,
523                     $options['tag']
524                 );
525             } else {
526                 $elements = $dom->getElementsByTagName($options['tag']);
527             }
528
529             foreach ($elements as $element) {
530                 $nodes[] = $element;
531             }
532
533             if (empty($nodes)) {
534                 return false;
535             }
536         } // no tag selected, get them all
537         else {
538             $tags = array(
539               'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
540               'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
541               'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
542               'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
543               'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
544               'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
545               'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
546               'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
547               'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
548               'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
549               'tr', 'tt', 'ul', 'var',
550               // HTML5
551               'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
552               'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
553               'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
554               'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
555               'source', 'summary', 'time', 'video', 'wbr'
556             );
557
558             foreach ($tags as $tag) {
559                 if ($isHtml) {
560                     $elements = self::getElementsByCaseInsensitiveTagName(
561                         $dom,
562                         $tag
563                     );
564                 } else {
565                     $elements = $dom->getElementsByTagName($tag);
566                 }
567
568                 foreach ($elements as $element) {
569                     $nodes[] = $element;
570                 }
571             }
572
573             if (empty($nodes)) {
574                 return false;
575             }
576         }
577
578         // filter by attributes
579         if ($options['attributes']) {
580             foreach ($nodes as $node) {
581                 $invalid = false;
582
583                 foreach ($options['attributes'] as $name => $value) {
584                     // match by regexp if like "regexp:/foo/i"
585                     if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
586                         if (!preg_match($matches[1], $node->getAttribute($name))) {
587                             $invalid = true;
588                         }
589                     } // class can match only a part
590                     elseif ($name == 'class') {
591                         // split to individual classes
592                         $findClasses = explode(
593                             ' ',
594                             preg_replace("/\s+/", ' ', $value)
595                         );
596
597                         $allClasses = explode(
598                             ' ',
599                             preg_replace("/\s+/", ' ', $node->getAttribute($name))
600                         );
601
602                         // make sure each class given is in the actual node
603                         foreach ($findClasses as $findClass) {
604                             if (!in_array($findClass, $allClasses)) {
605                                 $invalid = true;
606                             }
607                         }
608                     } // match by exact string
609                     else {
610                         if ($node->getAttribute($name) != $value) {
611                             $invalid = true;
612                         }
613                     }
614                 }
615
616                 // if every attribute given matched
617                 if (!$invalid) {
618                     $filtered[] = $node;
619                 }
620             }
621
622             $nodes    = $filtered;
623             $filtered = array();
624
625             if (empty($nodes)) {
626                 return false;
627             }
628         }
629
630         // filter by content
631         if ($options['content'] !== null) {
632             foreach ($nodes as $node) {
633                 $invalid = false;
634
635                 // match by regexp if like "regexp:/foo/i"
636                 if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
637                     if (!preg_match($matches[1], self::getNodeText($node))) {
638                         $invalid = true;
639                     }
640                 } // match empty string
641                 elseif ($options['content'] === '') {
642                     if (self::getNodeText($node) !== '') {
643                         $invalid = true;
644                     }
645                 } // match by exact string
646                 elseif (strstr(self::getNodeText($node), $options['content']) === false) {
647                     $invalid = true;
648                 }
649
650                 if (!$invalid) {
651                     $filtered[] = $node;
652                 }
653             }
654
655             $nodes    = $filtered;
656             $filtered = array();
657
658             if (empty($nodes)) {
659                 return false;
660             }
661         }
662
663         // filter by parent node
664         if ($options['parent']) {
665             $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
666             $parentNode  = isset($parentNodes[0]) ? $parentNodes[0] : null;
667
668             foreach ($nodes as $node) {
669                 if ($parentNode !== $node->parentNode) {
670                     continue;
671                 }
672
673                 $filtered[] = $node;
674             }
675
676             $nodes    = $filtered;
677             $filtered = array();
678
679             if (empty($nodes)) {
680                 return false;
681             }
682         }
683
684         // filter by child node
685         if ($options['child']) {
686             $childNodes = self::findNodes($dom, $options['child'], $isHtml);
687             $childNodes = !empty($childNodes) ? $childNodes : array();
688
689             foreach ($nodes as $node) {
690                 foreach ($node->childNodes as $child) {
691                     foreach ($childNodes as $childNode) {
692                         if ($childNode === $child) {
693                             $filtered[] = $node;
694                         }
695                     }
696                 }
697             }
698
699             $nodes    = $filtered;
700             $filtered = array();
701
702             if (empty($nodes)) {
703                 return false;
704             }
705         }
706
707         // filter by adjacent-sibling
708         if ($options['adjacent-sibling']) {
709             $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml);
710             $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array();
711
712             foreach ($nodes as $node) {
713                 $sibling = $node;
714
715                 while ($sibling = $sibling->nextSibling) {
716                     if ($sibling->nodeType !== XML_ELEMENT_NODE) {
717                         continue;
718                     }
719
720                     foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
721                         if ($sibling === $adjacentSiblingNode) {
722                             $filtered[] = $node;
723                             break;
724                         }
725                     }
726
727                     break;
728                 }
729             }
730
731             $nodes    = $filtered;
732             $filtered = array();
733
734             if (empty($nodes)) {
735                 return false;
736             }
737         }
738
739         // filter by ancestor
740         if ($options['ancestor']) {
741             $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
742             $ancestorNode  = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null;
743
744             foreach ($nodes as $node) {
745                 $parent = $node->parentNode;
746
747                 while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
748                     if ($parent === $ancestorNode) {
749                         $filtered[] = $node;
750                     }
751
752                     $parent = $parent->parentNode;
753                 }
754             }
755
756             $nodes    = $filtered;
757             $filtered = array();
758
759             if (empty($nodes)) {
760                 return false;
761             }
762         }
763
764         // filter by descendant
765         if ($options['descendant']) {
766             $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
767             $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
768
769             foreach ($nodes as $node) {
770                 foreach (self::getDescendants($node) as $descendant) {
771                     foreach ($descendantNodes as $descendantNode) {
772                         if ($descendantNode === $descendant) {
773                             $filtered[] = $node;
774                         }
775                     }
776                 }
777             }
778
779             $nodes    = $filtered;
780             $filtered = array();
781
782             if (empty($nodes)) {
783                 return false;
784             }
785         }
786
787         // filter by children
788         if ($options['children']) {
789             $validChild   = array('count', 'greater_than', 'less_than', 'only');
790             $childOptions = self::assertValidKeys(
791                 $options['children'],
792                 $validChild
793             );
794
795             foreach ($nodes as $node) {
796                 $childNodes = $node->childNodes;
797
798                 foreach ($childNodes as $childNode) {
799                     if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
800                         $childNode->nodeType !== XML_TEXT_NODE) {
801                         $children[] = $childNode;
802                     }
803                 }
804
805                 // we must have children to pass this filter
806                 if (!empty($children)) {
807                     // exact count of children
808                     if ($childOptions['count'] !== null) {
809                         if (count($children) !== $childOptions['count']) {
810                             break;
811                         }
812                     } // range count of children
813                     elseif ($childOptions['less_than']    !== null &&
814                             $childOptions['greater_than'] !== null) {
815                         if (count($children) >= $childOptions['less_than'] ||
816                             count($children) <= $childOptions['greater_than']) {
817                             break;
818                         }
819                     } // less than a given count
820                     elseif ($childOptions['less_than'] !== null) {
821                         if (count($children) >= $childOptions['less_than']) {
822                             break;
823                         }
824                     } // more than a given count
825                     elseif ($childOptions['greater_than'] !== null) {
826                         if (count($children) <= $childOptions['greater_than']) {
827                             break;
828                         }
829                     }
830
831                     // match each child against a specific tag
832                     if ($childOptions['only']) {
833                         $onlyNodes = self::findNodes(
834                             $dom,
835                             $childOptions['only'],
836                             $isHtml
837                         );
838
839                         // try to match each child to one of the 'only' nodes
840                         foreach ($children as $child) {
841                             $matched = false;
842
843                             foreach ($onlyNodes as $onlyNode) {
844                                 if ($onlyNode === $child) {
845                                     $matched = true;
846                                 }
847                             }
848
849                             if (!$matched) {
850                                 break 2;
851                             }
852                         }
853                     }
854
855                     $filtered[] = $node;
856                 }
857             }
858
859             $nodes = $filtered;
860
861             if (empty($nodes)) {
862                 return;
863             }
864         }
865
866         // return the first node that matches all criteria
867         return !empty($nodes) ? $nodes : array();
868     }
869
870     /**
871      * Recursively get flat array of all descendants of this node.
872      *
873      * @param DOMNode $node
874      *
875      * @return array
876      *
877      * @since  Method available since Release 3.3.0
878      */
879     protected static function getDescendants(DOMNode $node)
880     {
881         $allChildren = array();
882         $childNodes  = $node->childNodes ? $node->childNodes : array();
883
884         foreach ($childNodes as $child) {
885             if ($child->nodeType === XML_CDATA_SECTION_NODE ||
886                 $child->nodeType === XML_TEXT_NODE) {
887                 continue;
888             }
889
890             $children    = self::getDescendants($child);
891             $allChildren = array_merge($allChildren, $children, array($child));
892         }
893
894         return isset($allChildren) ? $allChildren : array();
895     }
896
897     /**
898      * Gets elements by case insensitive tagname.
899      *
900      * @param DOMDocument $dom
901      * @param string      $tag
902      *
903      * @return DOMNodeList
904      *
905      * @since  Method available since Release 3.4.0
906      */
907     protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag)
908     {
909         $elements = $dom->getElementsByTagName(strtolower($tag));
910
911         if ($elements->length == 0) {
912             $elements = $dom->getElementsByTagName(strtoupper($tag));
913         }
914
915         return $elements;
916     }
917
918     /**
919      * Get the text value of this node's child text node.
920      *
921      * @param DOMNode $node
922      *
923      * @return string
924      *
925      * @since  Method available since Release 3.3.0
926      */
927     protected static function getNodeText(DOMNode $node)
928     {
929         if (!$node->childNodes instanceof DOMNodeList) {
930             return '';
931         }
932
933         $result = '';
934
935         foreach ($node->childNodes as $childNode) {
936             if ($childNode->nodeType === XML_TEXT_NODE ||
937                 $childNode->nodeType === XML_CDATA_SECTION_NODE) {
938                 $result .= trim($childNode->data) . ' ';
939             } else {
940                 $result .= self::getNodeText($childNode);
941             }
942         }
943
944         return str_replace('  ', ' ', $result);
945     }
946 }