Completed
Push — master ( 95acd7...e70d94 )
by Naveen
13:02 queued 11:07
created

PHPUnit_Util_XML::findNodes()   F

Complexity

Conditions 88
Paths > 20000

Size

Total Lines 372
Code Lines 202

Duplication

Lines 56
Ratio 15.05 %
Metric Value
dl 56
loc 372
rs 2
cc 88
eloc 202
nc 429496.7295
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * This file is part of PHPUnit.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
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->getElementsByTagName('element') as $element) {
226
                    $item = $element->childNodes->item(0);
227
228
                    if ($item instanceof DOMText) {
229
                        $item = $element->childNodes->item(1);
230
                    }
231
232
                    $value = self::xmlToVariable($item);
233
234
                    if ($element->hasAttribute('key')) {
235
                        $variable[(string) $element->getAttribute('key')] = $value;
236
                    } else {
237
                        $variable[] = $value;
238
                    }
239
                }
240
                break;
241
242
            case 'object':
243
                $className = $element->getAttribute('class');
244
245
                if ($element->hasChildNodes()) {
246
                    $arguments       = $element->childNodes->item(1)->childNodes;
247
                    $constructorArgs = array();
248
249
                    foreach ($arguments as $argument) {
250
                        if ($argument instanceof DOMElement) {
251
                            $constructorArgs[] = self::xmlToVariable($argument);
252
                        }
253
                    }
254
255
                    $class    = new ReflectionClass($className);
256
                    $variable = $class->newInstanceArgs($constructorArgs);
257
                } else {
258
                    $variable = new $className;
259
                }
260
                break;
261
262
            case 'boolean':
263
                $variable = $element->textContent == 'true' ? true : false;
264
                break;
265
266
            case 'integer':
267
            case 'double':
268
            case 'string':
269
                $variable = $element->textContent;
270
271
                settype($variable, $element->tagName);
272
                break;
273
        }
274
275
        return $variable;
276
    }
277
278
    /**
279
     * Validate list of keys in the associative array.
280
     *
281
     * @param array $hash
282
     * @param array $validKeys
283
     *
284
     * @return array
285
     *
286
     * @throws PHPUnit_Framework_Exception
287
     *
288
     * @since  Method available since Release 3.3.0
289
     */
290
    public static function assertValidKeys(array $hash, array $validKeys)
291
    {
292
        $valids = array();
293
294
        // Normalize validation keys so that we can use both indexed and
295
        // associative arrays.
296
        foreach ($validKeys as $key => $val) {
297
            is_int($key) ? $valids[$val] = null : $valids[$key] = $val;
298
        }
299
300
        $validKeys = array_keys($valids);
301
302
        // Check for invalid keys.
303
        foreach ($hash as $key => $value) {
304
            if (!in_array($key, $validKeys)) {
305
                $unknown[] = $key;
306
            }
307
        }
308
309
        if (!empty($unknown)) {
310
            throw new PHPUnit_Framework_Exception(
311
                'Unknown key(s): ' . implode(', ', $unknown)
312
            );
313
        }
314
315
        // Add default values for any valid keys that are empty.
316
        foreach ($valids as $key => $value) {
317
            if (!isset($hash[$key])) {
318
                $hash[$key] = $value;
319
            }
320
        }
321
322
        return $hash;
323
    }
324
325
    /**
326
     * Parse a CSS selector into an associative array suitable for
327
     * use with findNodes().
328
     *
329
     * @param string $selector
330
     * @param mixed  $content
331
     *
332
     * @return array
333
     *
334
     * @since  Method available since Release 3.3.0
335
     */
336
    public static function convertSelectToTag($selector, $content = true)
337
    {
338
        $selector = trim(preg_replace("/\s+/", ' ', $selector));
339
340
        // substitute spaces within attribute value
341
        while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
342
            $selector = preg_replace(
343
                '/(\[[^\]]+"[^"]+)\s([^"]+"\])/',
344
                '$1__SPACE__$2',
345
                $selector
346
            );
347
        }
348
349
        if (strstr($selector, ' ')) {
350
            $elements = explode(' ', $selector);
351
        } else {
352
            $elements = array($selector);
353
        }
354
355
        $previousTag = array();
356
357
        foreach (array_reverse($elements) as $element) {
358
            $element = str_replace('__SPACE__', ' ', $element);
359
360
            // child selector
361
            if ($element == '>') {
362
                $previousTag = array('child' => $previousTag['descendant']);
363
                continue;
364
            }
365
366
            // adjacent-sibling selector
367
            if ($element == '+') {
368
                $previousTag = array('adjacent-sibling' => $previousTag['descendant']);
369
                continue;
370
            }
371
372
            $tag = array();
373
374
            // match element tag
375
            preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
376
377
            if (!empty($eltMatches[1])) {
378
                $tag['tag'] = $eltMatches[1];
379
            }
380
381
            // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*),
382
            // and classes (\.[^\.#\[]*))
383
            preg_match_all(
384
                "/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/",
385
                $element,
386
                $matches
387
            );
388
389
            if (!empty($matches[1])) {
390
                $classes = array();
391
                $attrs   = array();
392
393
                foreach ($matches[1] as $match) {
394
                    // id matched
395
                    if (substr($match, 0, 1) == '#') {
396
                        $tag['id'] = substr($match, 1);
397
                    } // class matched
398
                    elseif (substr($match, 0, 1) == '.') {
399
                        $classes[] = substr($match, 1);
400
                    } // attribute matched
401
                    elseif (substr($match, 0, 1) == '[' &&
402
                             substr($match, -1, 1) == ']') {
403
                        $attribute = substr($match, 1, strlen($match) - 2);
404
                        $attribute = str_replace('"', '', $attribute);
405
406
                        // match single word
407
                        if (strstr($attribute, '~=')) {
408
                            list($key, $value) = explode('~=', $attribute);
409
                            $value             = "regexp:/.*\b$value\b.*/";
410
                        } // match substring
411
                        elseif (strstr($attribute, '*=')) {
412
                            list($key, $value) = explode('*=', $attribute);
413
                            $value             = "regexp:/.*$value.*/";
414
                        } // exact match
415
                        else {
416
                            list($key, $value) = explode('=', $attribute);
417
                        }
418
419
                        $attrs[$key] = $value;
420
                    }
421
                }
422
423
                if (!empty($classes)) {
424
                    $tag['class'] = implode(' ', $classes);
425
                }
426
427
                if (!empty($attrs)) {
428
                    $tag['attributes'] = $attrs;
429
                }
430
            }
431
432
            // tag content
433
            if (is_string($content)) {
434
                $tag['content'] = $content;
435
            }
436
437
            // determine previous child/descendants
438
            if (!empty($previousTag['descendant'])) {
439
                $tag['descendant'] = $previousTag['descendant'];
440
            } elseif (!empty($previousTag['child'])) {
441
                $tag['child'] = $previousTag['child'];
442
            } elseif (!empty($previousTag['adjacent-sibling'])) {
443
                $tag['adjacent-sibling'] = $previousTag['adjacent-sibling'];
444
                unset($tag['content']);
445
            }
446
447
            $previousTag = array('descendant' => $tag);
448
        }
449
450
        return $tag;
451
    }
452
453
    /**
454
     * Parse an $actual document and return an array of DOMNodes
455
     * matching the CSS $selector.  If an error occurs, it will
456
     * return false.
457
     *
458
     * To only return nodes containing a certain content, give
459
     * the $content to match as a string.  Otherwise, setting
460
     * $content to true will return all nodes matching $selector.
461
     *
462
     * The $actual document may be a DOMDocument or a string
463
     * containing XML or HTML, identified by $isHtml.
464
     *
465
     * @param array  $selector
466
     * @param string $content
467
     * @param mixed  $actual
468
     * @param bool   $isHtml
469
     *
470
     * @return bool|array
471
     *
472
     * @since  Method available since Release 3.3.0
473
     */
474
    public static function cssSelect($selector, $content, $actual, $isHtml = true)
475
    {
476
        $matcher = self::convertSelectToTag($selector, $content);
477
        $dom     = self::load($actual, $isHtml);
478
        $tags    = self::findNodes($dom, $matcher, $isHtml);
479
480
        return $tags;
481
    }
482
483
    /**
484
     * Parse out the options from the tag using DOM object tree.
485
     *
486
     * @param DOMDocument $dom
487
     * @param array       $options
488
     * @param bool        $isHtml
489
     *
490
     * @return array
491
     *
492
     * @since  Method available since Release 3.3.0
493
     */
494
    public static function findNodes(DOMDocument $dom, array $options, $isHtml = true)
495
    {
496
        $valid = array(
497
          'id', 'class', 'tag', 'content', 'attributes', 'parent',
498
          'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
499
        );
500
501
        $filtered = array();
502
        $options  = self::assertValidKeys($options, $valid);
503
504
        // find the element by id
505
        if ($options['id']) {
506
            $options['attributes']['id'] = $options['id'];
507
        }
508
509
        if ($options['class']) {
510
            $options['attributes']['class'] = $options['class'];
511
        }
512
513
        $nodes = array();
514
515
        // find the element by a tag type
516
        if ($options['tag']) {
517
            if ($isHtml) {
518
                $elements = self::getElementsByCaseInsensitiveTagName(
519
                    $dom,
520
                    $options['tag']
521
                );
522
            } else {
523
                $elements = $dom->getElementsByTagName($options['tag']);
524
            }
525
526
            foreach ($elements as $element) {
527
                $nodes[] = $element;
528
            }
529
530
            if (empty($nodes)) {
531
                return false;
532
            }
533
        } // no tag selected, get them all
534
        else {
535
            $tags = array(
536
              'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
537
              'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
538
              'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
539
              'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
540
              'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
541
              'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
542
              'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
543
              'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
544
              'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
545
              'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
546
              'tr', 'tt', 'ul', 'var',
547
              // HTML5
548
              'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
549
              'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
550
              'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
551
              'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
552
              'source', 'summary', 'time', 'video', 'wbr'
553
            );
554
555
            foreach ($tags as $tag) {
556
                if ($isHtml) {
557
                    $elements = self::getElementsByCaseInsensitiveTagName(
558
                        $dom,
559
                        $tag
560
                    );
561
                } else {
562
                    $elements = $dom->getElementsByTagName($tag);
563
                }
564
565
                foreach ($elements as $element) {
566
                    $nodes[] = $element;
567
                }
568
            }
569
570
            if (empty($nodes)) {
571
                return false;
572
            }
573
        }
574
575
        // filter by attributes
576
        if ($options['attributes']) {
577
            foreach ($nodes as $node) {
578
                $invalid = false;
579
580
                foreach ($options['attributes'] as $name => $value) {
581
                    // match by regexp if like "regexp:/foo/i"
582
                    if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
583
                        if (!preg_match($matches[1], $node->getAttribute($name))) {
584
                            $invalid = true;
585
                        }
586
                    } // class can match only a part
587
                    elseif ($name == 'class') {
588
                        // split to individual classes
589
                        $findClasses = explode(
590
                            ' ',
591
                            preg_replace("/\s+/", ' ', $value)
592
                        );
593
594
                        $allClasses = explode(
595
                            ' ',
596
                            preg_replace("/\s+/", ' ', $node->getAttribute($name))
597
                        );
598
599
                        // make sure each class given is in the actual node
600
                        foreach ($findClasses as $findClass) {
601
                            if (!in_array($findClass, $allClasses)) {
602
                                $invalid = true;
603
                            }
604
                        }
605
                    } // match by exact string
606
                    else {
607
                        if ($node->getAttribute($name) != $value) {
608
                            $invalid = true;
609
                        }
610
                    }
611
                }
612
613
                // if every attribute given matched
614
                if (!$invalid) {
615
                    $filtered[] = $node;
616
                }
617
            }
618
619
            $nodes    = $filtered;
620
            $filtered = array();
621
622
            if (empty($nodes)) {
623
                return false;
624
            }
625
        }
626
627
        // filter by content
628
        if ($options['content'] !== null) {
629
            foreach ($nodes as $node) {
630
                $invalid = false;
631
632
                // match by regexp if like "regexp:/foo/i"
633
                if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
634
                    if (!preg_match($matches[1], self::getNodeText($node))) {
635
                        $invalid = true;
636
                    }
637
                } // match empty string
638
                elseif ($options['content'] === '') {
639
                    if (self::getNodeText($node) !== '') {
640
                        $invalid = true;
641
                    }
642
                } // match by exact string
643
                elseif (strstr(self::getNodeText($node), $options['content']) === false) {
644
                    $invalid = true;
645
                }
646
647
                if (!$invalid) {
648
                    $filtered[] = $node;
649
                }
650
            }
651
652
            $nodes    = $filtered;
653
            $filtered = array();
654
655
            if (empty($nodes)) {
656
                return false;
657
            }
658
        }
659
660
        // filter by parent node
661
        if ($options['parent']) {
662
            $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
663
            $parentNode  = isset($parentNodes[0]) ? $parentNodes[0] : null;
664
665
            foreach ($nodes as $node) {
666
                if ($parentNode !== $node->parentNode) {
667
                    continue;
668
                }
669
670
                $filtered[] = $node;
671
            }
672
673
            $nodes    = $filtered;
674
            $filtered = array();
675
676
            if (empty($nodes)) {
677
                return false;
678
            }
679
        }
680
681
        // filter by child node
682
        if ($options['child']) {
683
            $childNodes = self::findNodes($dom, $options['child'], $isHtml);
684
            $childNodes = !empty($childNodes) ? $childNodes : array();
685
686
            foreach ($nodes as $node) {
687
                foreach ($node->childNodes as $child) {
688
                    foreach ($childNodes as $childNode) {
689
                        if ($childNode === $child) {
690
                            $filtered[] = $node;
691
                        }
692
                    }
693
                }
694
            }
695
696
            $nodes    = $filtered;
697
            $filtered = array();
698
699
            if (empty($nodes)) {
700
                return false;
701
            }
702
        }
703
704
        // filter by adjacent-sibling
705
        if ($options['adjacent-sibling']) {
706
            $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml);
707
            $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array();
708
709
            foreach ($nodes as $node) {
710
                $sibling = $node;
711
712
                while ($sibling = $sibling->nextSibling) {
713
                    if ($sibling->nodeType !== XML_ELEMENT_NODE) {
714
                        continue;
715
                    }
716
717
                    foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
718
                        if ($sibling === $adjacentSiblingNode) {
719
                            $filtered[] = $node;
720
                            break;
721
                        }
722
                    }
723
724
                    break;
725
                }
726
            }
727
728
            $nodes    = $filtered;
729
            $filtered = array();
730
731
            if (empty($nodes)) {
732
                return false;
733
            }
734
        }
735
736
        // filter by ancestor
737
        if ($options['ancestor']) {
738
            $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
739
            $ancestorNode  = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null;
740
741
            foreach ($nodes as $node) {
742
                $parent = $node->parentNode;
743
744
                while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
745
                    if ($parent === $ancestorNode) {
746
                        $filtered[] = $node;
747
                    }
748
749
                    $parent = $parent->parentNode;
750
                }
751
            }
752
753
            $nodes    = $filtered;
754
            $filtered = array();
755
756
            if (empty($nodes)) {
757
                return false;
758
            }
759
        }
760
761
        // filter by descendant
762
        if ($options['descendant']) {
763
            $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
764
            $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
765
766
            foreach ($nodes as $node) {
767
                foreach (self::getDescendants($node) as $descendant) {
768
                    foreach ($descendantNodes as $descendantNode) {
769
                        if ($descendantNode === $descendant) {
770
                            $filtered[] = $node;
771
                        }
772
                    }
773
                }
774
            }
775
776
            $nodes    = $filtered;
777
            $filtered = array();
778
779
            if (empty($nodes)) {
780
                return false;
781
            }
782
        }
783
784
        // filter by children
785
        if ($options['children']) {
786
            $validChild   = array('count', 'greater_than', 'less_than', 'only');
787
            $childOptions = self::assertValidKeys(
788
                $options['children'],
789
                $validChild
790
            );
791
792
            foreach ($nodes as $node) {
793
                $childNodes = $node->childNodes;
794
795
                foreach ($childNodes as $childNode) {
796
                    if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
797
                        $childNode->nodeType !== XML_TEXT_NODE) {
798
                        $children[] = $childNode;
799
                    }
800
                }
801
802
                // we must have children to pass this filter
803
                if (!empty($children)) {
804
                    // exact count of children
805
                    if ($childOptions['count'] !== null) {
806
                        if (count($children) !== $childOptions['count']) {
807
                            break;
808
                        }
809
                    } // range count of children
810
                    elseif ($childOptions['less_than']    !== null &&
811
                            $childOptions['greater_than'] !== null) {
812
                        if (count($children) >= $childOptions['less_than'] ||
813
                            count($children) <= $childOptions['greater_than']) {
814
                            break;
815
                        }
816
                    } // less than a given count
817
                    elseif ($childOptions['less_than'] !== null) {
818
                        if (count($children) >= $childOptions['less_than']) {
819
                            break;
820
                        }
821
                    } // more than a given count
822
                    elseif ($childOptions['greater_than'] !== null) {
823
                        if (count($children) <= $childOptions['greater_than']) {
824
                            break;
825
                        }
826
                    }
827
828
                    // match each child against a specific tag
829
                    if ($childOptions['only']) {
830
                        $onlyNodes = self::findNodes(
831
                            $dom,
832
                            $childOptions['only'],
833
                            $isHtml
834
                        );
835
836
                        // try to match each child to one of the 'only' nodes
837
                        foreach ($children as $child) {
838
                            $matched = false;
839
840
                            foreach ($onlyNodes as $onlyNode) {
841
                                if ($onlyNode === $child) {
842
                                    $matched = true;
843
                                }
844
                            }
845
846
                            if (!$matched) {
847
                                break 2;
848
                            }
849
                        }
850
                    }
851
852
                    $filtered[] = $node;
853
                }
854
            }
855
856
            $nodes = $filtered;
857
858
            if (empty($nodes)) {
859
                return;
860
            }
861
        }
862
863
        // return the first node that matches all criteria
864
        return !empty($nodes) ? $nodes : array();
865
    }
866
867
    /**
868
     * Recursively get flat array of all descendants of this node.
869
     *
870
     * @param DOMNode $node
871
     *
872
     * @return array
873
     *
874
     * @since  Method available since Release 3.3.0
875
     */
876
    protected static function getDescendants(DOMNode $node)
877
    {
878
        $allChildren = array();
879
        $childNodes  = $node->childNodes ? $node->childNodes : array();
880
881
        foreach ($childNodes as $child) {
882
            if ($child->nodeType === XML_CDATA_SECTION_NODE ||
883
                $child->nodeType === XML_TEXT_NODE) {
884
                continue;
885
            }
886
887
            $children    = self::getDescendants($child);
888
            $allChildren = array_merge($allChildren, $children, array($child));
889
        }
890
891
        return isset($allChildren) ? $allChildren : array();
892
    }
893
894
    /**
895
     * Gets elements by case insensitive tagname.
896
     *
897
     * @param DOMDocument $dom
898
     * @param string      $tag
899
     *
900
     * @return DOMNodeList
901
     *
902
     * @since  Method available since Release 3.4.0
903
     */
904
    protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag)
905
    {
906
        $elements = $dom->getElementsByTagName(strtolower($tag));
907
908
        if ($elements->length == 0) {
909
            $elements = $dom->getElementsByTagName(strtoupper($tag));
910
        }
911
912
        return $elements;
913
    }
914
915
    /**
916
     * Get the text value of this node's child text node.
917
     *
918
     * @param DOMNode $node
919
     *
920
     * @return string
921
     *
922
     * @since  Method available since Release 3.3.0
923
     */
924
    protected static function getNodeText(DOMNode $node)
925
    {
926
        if (!$node->childNodes instanceof DOMNodeList) {
927
            return '';
928
        }
929
930
        $result = '';
931
932
        foreach ($node->childNodes as $childNode) {
933
            if ($childNode->nodeType === XML_TEXT_NODE ||
934
                $childNode->nodeType === XML_CDATA_SECTION_NODE) {
935
                $result .= trim($childNode->data) . ' ';
936
            } else {
937
                $result .= self::getNodeText($childNode);
938
            }
939
        }
940
941
        return str_replace('  ', ' ', $result);
942
    }
943
}
944