DOMTraverser::matchAttributes()   C
last analyzed

Complexity

Conditions 14
Paths 16

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 21
c 1
b 0
f 0
nc 16
nop 2
dl 0
loc 38
rs 6.2666

How to fix   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
/** @file
3
 * Traverse a DOM.
4
 */
5
6
namespace QueryPath\CSS;
7
8
use \QueryPath\CSS\DOMTraverser\Util;
9
use \QueryPath\CSS\DOMTraverser\PseudoClass;
10
use \QueryPath\CSS\DOMTraverser\PseudoElement;
0 ignored issues
show
Bug introduced by
The type QueryPath\CSS\DOMTraverser\PseudoElement was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use QueryPath\CSS\SimpleSelector;
12
use SplObjectStorage;
13
14
/**
15
 * Traverse a DOM, finding matches to the selector.
16
 *
17
 * This traverses a DOMDocument and attempts to find
18
 * matches to the provided selector.
19
 *
20
 * \b How this works
21
 *
22
 * This performs a bottom-up search. On the first pass,
23
 * it attempts to find all of the matching elements for the
24
 * last simple selector in a selector.
25
 *
26
 * Subsequent passes attempt to eliminate matches from the
27
 * initial matching set.
28
 *
29
 * Example:
30
 *
31
 * Say we begin with the selector `foo.bar baz`. This is processed
32
 * as follows:
33
 *
34
 * - First, find all baz elements.
35
 * - Next, for any baz element that does not have foo as an ancestor,
36
 *   eliminate it from the matches.
37
 * - Finally, for those that have foo as an ancestor, does that foo
38
 *   also have a class baz? If not, it is removed from the matches.
39
 *
40
 * \b Extrapolation
41
 *
42
 * Partial simple selectors are almost always expanded to include an
43
 * element.
44
 *
45
 * Examples:
46
 *
47
 * - `:first` is expanded to `*:first`
48
 * - `.bar` is expanded to `*.bar`.
49
 * - `.outer .inner` is expanded to `*.outer *.inner`
50
 *
51
 * The exception is that IDs are sometimes not expanded, e.g.:
52
 *
53
 * - `#myElement` does not get expanded
54
 * - `#myElement .class` \i may be expanded to `*#myElement *.class`
55
 *   (which will obviously not perform well).
56
 */
57
class DOMTraverser implements Traverser
58
{
59
    protected $matches     = [];
60
    protected $selector;
61
    protected $dom;
62
    protected $initialized = true;
63
    protected $psHandler;
64
    protected $scopeNode;
65
66
    /**
67
     * Build a new DOMTraverser.
68
     *
69
     * This requires a DOM-like object or collection of DOM nodes.
70
     *
71
     * @param \SPLObjectStorage $splos
72
     * @param bool $initialized
73
     * @param null $scopeNode
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $scopeNode is correct as it would always require null to be passed?
Loading history...
74
     */
75
    public function __construct(\SPLObjectStorage $splos, bool $initialized = false, $scopeNode = NULL)
76
    {
77
        $this->psHandler   = new PseudoClass();
78
        $this->initialized = $initialized;
79
80
        // Re-use the initial splos
81
        $this->matches = $splos;
82
83
        if (count($splos) !== 0) {
84
            $splos->rewind();
85
            $first = $splos->current();
86
            if ($first instanceof \DOMDocument) {
87
                $this->dom = $first;//->documentElement;
88
            } else {
89
                $this->dom = $first->ownerDocument;//->documentElement;
90
            }
91
92
            $this->scopeNode = $scopeNode;
93
            if (empty($scopeNode)) {
94
                $this->scopeNode = $this->dom->documentElement;
95
            }
96
        }
97
98
        // This assumes a DOM. Need to also accomodate the case
99
        // where we get a set of elements.
100
        /*
101
        $this->dom = $dom;
102
        $this->matches = new \SplObjectStorage();
103
        $this->matches->attach($this->dom);
104
         */
105
    }
106
107
    public function debug($msg)
108
    {
109
        fwrite(STDOUT, PHP_EOL . $msg);
110
    }
111
112
    /**
113
     * Given a selector, find the matches in the given DOM.
114
     *
115
     * This is the main function for querying the DOM using a CSS
116
     * selector.
117
     *
118
     * @param string $selector
119
     *   The selector.
120
     * @return DOMTraverser a list of matched
121
     *   DOMNode objects.
122
     * @throws ParseException
123
     */
124
    public function find($selector) : DOMTraverser
125
    {
126
        // Setup
127
        $handler = new Selector();
128
        $parser  = new Parser($selector, $handler);
129
        $parser->parse();
130
        $this->selector = $handler;
131
132
        //$selector = $handler->toArray();
133
        $found = $this->newMatches();
134
        foreach ($handler as $selectorGroup) {
135
            // Initialize matches if necessary.
136
            if ($this->initialized) {
137
                $candidates = $this->matches;
138
            } else {
139
                $candidates = $this->initialMatch($selectorGroup[0], $this->matches);
140
            }
141
142
            /** @var \DOMElement $candidate */
143
            foreach ($candidates as $candidate) {
144
                // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]);
145
                if ($this->matchesSelector($candidate, $selectorGroup)) {
146
                    // $this->debug('Attaching ' . $candidate->nodeName);
147
                    $found->attach($candidate);
148
                }
149
            }
150
        }
151
        $this->setMatches($found);
152
153
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\CSS\DOMTraverser which is incompatible with the return type mandated by QueryPath\CSS\Traverser::find() of Traverser.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
154
    }
155
156
    public function matches()
157
    {
158
        return $this->matches;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->matches returns the type SPLObjectStorage which is incompatible with the return type mandated by QueryPath\CSS\Traverser::matches() of array.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
159
    }
160
161
    /**
162
     * Check whether the given node matches the given selector.
163
     *
164
     * A selector is a group of one or more simple selectors combined
165
     * by combinators. This determines if a given selector
166
     * matches the given node.
167
     *
168
     * @attention
169
     * Evaluation of selectors is done recursively. Thus the length
170
     * of the selector is limited to the recursion depth allowed by
171
     * the PHP configuration. This should only cause problems for
172
     * absolutely huge selectors or for versions of PHP tuned to
173
     * strictly limit recursion depth.
174
     *
175
     * @param \DOMElement $node
176
     *   The DOMNode to check.
177
     * @param $selector
178
     * @return boolean
179
     *   A boolean TRUE if the node matches, false otherwise.
180
     */
181
    public function matchesSelector(\DOMElement $node, $selector)
182
    {
183
        return $this->matchesSimpleSelector($node, $selector, 0);
184
    }
185
186
    /**
187
     * Performs a match check on a SimpleSelector.
188
     *
189
     * Where matchesSelector() does a check on an entire selector,
190
     * this checks only a simple selector (plus an optional
191
     * combinator).
192
     *
193
     * @param \DOMElement $node
194
     * @param $selectors
195
     * @param $index
196
     * @return boolean
197
     *   A boolean TRUE if the node matches, false otherwise.
198
     * @throws NotImplementedException
199
     */
200
    public function matchesSimpleSelector(\DOMElement $node, $selectors, $index)
201
    {
202
        $selector = $selectors[$index];
203
        // Note that this will short circuit as soon as one of these
204
        // returns FALSE.
205
        $result = $this->matchElement($node, $selector->element, $selector->ns)
206
            && $this->matchAttributes($node, $selector->attributes)
207
            && $this->matchId($node, $selector->id)
208
            && $this->matchClasses($node, $selector->classes)
209
            && $this->matchPseudoClasses($node, $selector->pseudoClasses)
210
            && $this->matchPseudoElements($node, $selector->pseudoElements);
211
212
        $isNextRule = isset($selectors[++$index]);
213
        // If there is another selector, we process that if there a match
214
        // hasn't been found.
215
        /*
216
        if ($isNextRule && $selectors[$index]->combinator == SimpleSelector::anotherSelector) {
217
          // We may need to re-initialize the match set for the next selector.
218
          if (!$this->initialized) {
219
            $this->initialMatch($selectors[$index]);
220
          }
221
          if (!$result) fprintf(STDOUT, "Element: %s, Next selector: %s\n", $node->tagName, $selectors[$index]);
222
          return $result || $this->matchesSimpleSelector($node, $selectors, $index);
223
        }
224
        // If we have a match and we have a combinator, we need to
225
        // recurse up the tree.
226
        else*/
227
        if ($isNextRule && $result) {
228
            $result = $this->combine($node, $selectors, $index);
229
        }
230
231
        return $result;
232
    }
233
234
    /**
235
     * Combine the next selector with the given match
236
     * using the next combinator.
237
     *
238
     * If the next selector is combined with another
239
     * selector, that will be evaluated too, and so on.
240
     * So if this function returns TRUE, it means that all
241
     * child selectors are also matches.
242
     *
243
     * @param DOMNode $node
0 ignored issues
show
Bug introduced by
The type QueryPath\CSS\DOMNode was not found. Did you mean DOMNode? If so, make sure to prefix the type with \.
Loading history...
244
     *   The DOMNode to test.
245
     * @param array $selectors
246
     *   The array of simple selectors.
247
     * @param int $index
248
     *   The index of the current selector.
249
     * @return boolean
250
     *   TRUE if the next selector(s) match.
251
     */
252
    public function combine(\DOMElement $node, $selectors, $index)
253
    {
254
        $selector = $selectors[$index];
255
        //$this->debug(implode(' ', $selectors));
256
        switch ($selector->combinator) {
257
            case SimpleSelector::ADJACENT:
258
                return $this->combineAdjacent($node, $selectors, $index);
259
            case SimpleSelector::SIBLING:
260
                return $this->combineSibling($node, $selectors, $index);
261
            case SimpleSelector::DIRECT_DESCENDANT:
262
                return $this->combineDirectDescendant($node, $selectors, $index);
263
            case SimpleSelector::ANY_DESCENDANT:
264
                return $this->combineAnyDescendant($node, $selectors, $index);
265
            case SimpleSelector::ANOTHER_SELECTOR:
266
                // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]);
267
                return $this->matchesSimpleSelector($node, $selectors, $index);;
268
        }
269
270
        return false;
271
    }
272
273
    /**
274
     * Process an Adjacent Sibling.
275
     *
276
     * The spec does not indicate whether Adjacent should ignore non-Element
277
     * nodes, so we choose to ignore them.
278
     *
279
     * @param DOMNode $node
280
     *   A DOM Node.
281
     * @param array $selectors
282
     *   The selectors array.
283
     * @param int $index
284
     *   The current index to the operative simple selector in the selectors
285
     *   array.
286
     * @return boolean
287
     *   TRUE if the combination matches, FALSE otherwise.
288
     */
289
    public function combineAdjacent($node, $selectors, $index)
290
    {
291
        while (!empty($node->previousSibling)) {
292
            $node = $node->previousSibling;
293
            if ($node->nodeType == XML_ELEMENT_NODE) {
294
                //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index]));
295
                return $this->matchesSimpleSelector($node, $selectors, $index);
296
            }
297
        }
298
299
        return false;
300
    }
301
302
    /**
303
     * Check all siblings.
304
     *
305
     * According to the spec, this only tests elements LEFT of the provided
306
     * node.
307
     *
308
     * @param DOMNode $node
309
     *   A DOM Node.
310
     * @param array $selectors
311
     *   The selectors array.
312
     * @param int $index
313
     *   The current index to the operative simple selector in the selectors
314
     *   array.
315
     * @return boolean
316
     *   TRUE if the combination matches, FALSE otherwise.
317
     */
318
    public function combineSibling($node, $selectors, $index)
319
    {
320
        while (!empty($node->previousSibling)) {
321
            $node = $node->previousSibling;
322
            if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) {
323
                return true;
324
            }
325
        }
326
327
        return false;
328
    }
329
330
    /**
331
     * Handle a Direct Descendant combination.
332
     *
333
     * Check whether the given node is a rightly-related descendant
334
     * of its parent node.
335
     *
336
     * @param DOMNode $node
337
     *   A DOM Node.
338
     * @param array $selectors
339
     *   The selectors array.
340
     * @param int $index
341
     *   The current index to the operative simple selector in the selectors
342
     *   array.
343
     * @return boolean
344
     *   TRUE if the combination matches, FALSE otherwise.
345
     */
346
    public function combineDirectDescendant($node, $selectors, $index)
347
    {
348
        $parent = $node->parentNode;
349
        if (empty($parent)) {
350
            return false;
351
        }
352
353
        return $this->matchesSimpleSelector($parent, $selectors, $index);
354
    }
355
356
    /**
357
     * Handle Any Descendant combinations.
358
     *
359
     * This checks to see if there are any matching routes from the
360
     * selector beginning at the present node.
361
     *
362
     * @param DOMNode $node
363
     *   A DOM Node.
364
     * @param array $selectors
365
     *   The selectors array.
366
     * @param int $index
367
     *   The current index to the operative simple selector in the selectors
368
     *   array.
369
     * @return boolean
370
     *   TRUE if the combination matches, FALSE otherwise.
371
     */
372
    public function combineAnyDescendant($node, $selectors, $index)
373
    {
374
        while (!empty($node->parentNode)) {
375
            $node = $node->parentNode;
376
377
            // Catch case where element is child of something
378
            // else. This should really only happen with a
379
            // document element.
380
            if ($node->nodeType != XML_ELEMENT_NODE) {
381
                continue;
382
            }
383
384
            if ($this->matchesSimpleSelector($node, $selectors, $index)) {
385
                return true;
386
            }
387
        }
388
    }
389
390
    /**
391
     * Get the intial match set.
392
     *
393
     * This should only be executed when not working with
394
     * an existing match set.
395
     * @param \QueryPath\CSS\SimpleSelector $selector
396
     * @param SplObjectStorage $matches
397
     * @return SplObjectStorage
398
     */
399
    protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches) : SplObjectStorage
400
    {
401
        $element = $selector->element;
402
403
        // If no element is specified, we have to start with the
404
        // entire document.
405
        if ($element === NULL) {
406
            $element = '*';
407
        }
408
409
        // We try to do some optimization here to reduce the
410
        // number of matches to the bare minimum. This will
411
        // reduce the subsequent number of operations that
412
        // must be performed in the query.
413
414
        // Experimental: ID queries use XPath to match, since
415
        // this should give us only a single matched element
416
        // to work with.
417
        if (/*$element == '*' &&*/
418
        !empty($selector->id)) {
419
            $initialMatches = $this->initialMatchOnID($selector, $matches);
420
        } // If a namespace is set, find the namespace matches.
421
        elseif (!empty($selector->ns)) {
422
            $initialMatches = $this->initialMatchOnElementNS($selector, $matches);
423
        }
424
        // If the element is a wildcard, using class can
425
        // substantially reduce the number of elements that
426
        // we start with.
427
        elseif ($element === '*' && !empty($selector->classes)) {
428
            $initialMatches = $this->initialMatchOnClasses($selector, $matches);
429
        } else {
430
            $initialMatches = $this->initialMatchOnElement($selector, $matches);
431
        }
432
433
        return $initialMatches;
434
    }
435
436
    /**
437
     * Shortcut for finding initial match by ID.
438
     *
439
     * If the element is set to '*' and an ID is
440
     * set, then this should be used to find by ID,
441
     * which will drastically reduce the amount of
442
     * comparison operations done in PHP.
443
     * @param \QueryPath\CSS\SimpleSelector $selector
444
     * @param SplObjectStorage $matches
445
     * @return SplObjectStorage
446
     */
447
    protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches) : SplObjectStorage
448
    {
449
        $id    = $selector->id;
450
        $found = $this->newMatches();
451
452
        // Issue #145: DOMXPath will through an exception if the DOM is
453
        // not set.
454
        if (!($this->dom instanceof \DOMDocument)) {
455
            return $found;
456
        }
457
        $baseQuery = ".//*[@id='{$id}']";
458
        $xpath     = new \DOMXPath($this->dom);
459
460
        // Now we try to find any matching IDs.
461
        /** @var \DOMElement $node */
462
        foreach ($matches as $node) {
463
            if ($node->getAttribute('id') === $id) {
464
                $found->attach($node);
465
            }
466
467
            $nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
468
            if (!empty($nl) && $nl instanceof \DOMNodeList) {
469
                $this->attachNodeList($nl, $found);
470
            }
471
        }
472
        // Unset the ID selector.
473
        $selector->id = NULL;
474
475
        return $found;
476
    }
477
478
    /**
479
     * Shortcut for setting the intial match.
480
     *
481
     * This shortcut should only be used when the initial
482
     * element is '*' and there are classes set.
483
     *
484
     * In any other case, the element finding algo is
485
     * faster and should be used instead.
486
     * @param \QueryPath\CSS\SimpleSelector $selector
487
     * @param $matches
488
     * @return \SplObjectStorage
489
     */
490
    protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches) : \SplObjectStorage
491
    {
492
        $found = $this->newMatches();
493
494
        // Issue #145: DOMXPath will through an exception if the DOM is
495
        // not set.
496
        if (!($this->dom instanceof \DOMDocument)) {
497
            return $found;
498
        }
499
        $baseQuery = './/*[@class]';
500
        $xpath     = new \DOMXPath($this->dom);
501
502
        // Now we try to find any matching IDs.
503
        /** @var \DOMElement $node */
504
        foreach ($matches as $node) {
505
            // Refactor me!
506
            if ($node->hasAttribute('class')) {
507
                $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class')));
508
                if (count($intersect) === count($selector->classes)) {
509
                    $found->attach($node);
510
                }
511
            }
512
513
            $nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
514
            /** @var \DOMElement $subNode */
515
            foreach ($nl as $subNode) {
516
                $classes    = $subNode->getAttribute('class');
517
                $classArray = explode(' ', $classes);
518
519
                $intersect = array_intersect($selector->classes, $classArray);
520
                if (count($intersect) === count($selector->classes)) {
521
                    $found->attach($subNode);
522
                }
523
            }
524
        }
525
526
        // Unset the classes selector.
527
        $selector->classes = [];
528
529
        return $found;
530
    }
531
532
    /**
533
     * Internal xpath query.
534
     *
535
     * This is optimized for very specific use, and is not a general
536
     * purpose function.
537
     * @param \DOMXPath $xpath
538
     * @param \DOMElement $node
539
     * @param string $query
540
     * @return \DOMNodeList
541
     */
542
    private function initialXpathQuery(\DOMXPath $xpath, \DOMElement $node, string $query) : \DOMNodeList
543
    {
544
        // This works around a bug in which the document element
545
        // does not correctly search with the $baseQuery.
546
        if ($node->isSameNode($this->dom->documentElement)) {
547
            $query = mb_substr($query, 1);
548
        }
549
550
        return $xpath->query($query, $node);
551
    }
552
553
    /**
554
     * Shortcut for setting the initial match.
555
     *
556
     * @param $selector
557
     * @param $matches
558
     * @return \SplObjectStorage
559
     */
560
    protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches) : SplObjectStorage
561
    {
562
        $element = $selector->element;
563
        if (NULL === $element) {
564
            $element = '*';
565
        }
566
        $found = $this->newMatches();
567
        /** @var \DOMDocument $node */
568
        foreach ($matches as $node) {
569
            // Capture the case where the initial element is the root element.
570
            if ($node->tagName === $element
571
                || ($element === '*' && $node->parentNode instanceof \DOMDocument)) {
572
                $found->attach($node);
573
            }
574
            $nl = $node->getElementsByTagName($element);
575
            if (!empty($nl) && $nl instanceof \DOMNodeList) {
576
                $this->attachNodeList($nl, $found);
577
            }
578
        }
579
580
        $selector->element = NULL;
581
582
        return $found;
583
    }
584
585
    /**
586
     * Get elements and filter by namespace.
587
     * @param \QueryPath\CSS\SimpleSelector $selector
588
     * @param SplObjectStorage $matches
589
     * @return SplObjectStorage
590
     */
591
    protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches) : SplObjectStorage
592
    {
593
        $ns = $selector->ns;
594
595
        $elements = $this->initialMatchOnElement($selector, $matches);
596
597
        // "any namespace" matches anything.
598
        if ($ns === '*') {
599
            return $elements;
600
        }
601
602
        // Loop through and make a list of items that need to be filtered
603
        // out, then filter them. This is required b/c ObjectStorage iterates
604
        // wrongly when an item is detached in an access loop.
605
        $detach = [];
606
        foreach ($elements as $node) {
607
            // This lookup must be done PER NODE.
608
            $nsuri = $node->lookupNamespaceURI($ns);
609
            if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
610
                $detach[] = $node;
611
            }
612
        }
613
        foreach ($detach as $rem) {
614
            $elements->detach($rem);
615
        }
616
        $selector->ns = NULL;
617
618
        return $elements;
619
    }
620
621
    /**
622
     * Checks to see if the DOMNode matches the given element selector.
623
     *
624
     * This handles the following cases:
625
     *
626
     * - element (foo)
627
     * - namespaced element (ns|foo)
628
     * - namespaced wildcard (ns|*)
629
     * - wildcard (* or *|*)
630
     * @param \DOMElement $node
631
     * @param $element
632
     * @param null $ns
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $ns is correct as it would always require null to be passed?
Loading history...
633
     * @return bool
634
     */
635
    protected function matchElement(\DOMElement $node, $element, $ns = NULL) : bool
636
    {
637
        if (empty($element)) {
638
            return true;
639
        }
640
641
        // Handle namespace.
642
        if (!empty($ns) && $ns !== '*') {
643
            // Check whether we have a matching NS URI.
644
            $nsuri = $node->lookupNamespaceURI($ns);
645
            if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
646
                return false;
647
            }
648
        }
649
650
        // Compare local name to given element name.
651
        return $element === '*' || $node->localName === $element;
652
    }
653
654
    /**
655
     * Checks to see if the given DOMNode matches an "any element" (*).
656
     *
657
     * This does not handle namespaced whildcards.
658
     */
659
    /*
660
    protected function matchAnyElement($node) {
661
      $ancestors = $this->ancestors($node);
662
663
      return count($ancestors) > 0;
664
    }
665
     */
666
667
    /**
668
     * Get a list of ancestors to the present node.
669
     */
670
    protected function ancestors($node)
671
    {
672
        $buffer = [];
673
        $parent = $node;
674
        while (($parent = $parent->parentNode) !== NULL) {
675
            $buffer[] = $parent;
676
        }
677
678
        return $buffer;
679
    }
680
681
    /**
682
     * Check to see if DOMNode has all of the given attributes.
683
     *
684
     * This can handle namespaced attributes, including namespace
685
     * wildcards.
686
     * @param \DOMElement $node
687
     * @param $attributes
688
     * @return bool
689
     */
690
    protected function matchAttributes(\DOMElement $node, $attributes) : bool
691
    {
692
        if (empty($attributes)) {
693
            return true;
694
        }
695
696
        foreach ($attributes as $attr) {
697
            $val = isset($attr['value']) ? $attr['value'] : NULL;
698
699
            // Namespaced attributes.
700
            if (isset($attr['ns']) && $attr['ns'] !== '*') {
701
                $nsuri = $node->lookupNamespaceURI($attr['ns']);
702
                if (empty($nsuri) || !$node->hasAttributeNS($nsuri, $attr['name'])) {
703
                    return false;
704
                }
705
                $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']);
706
            } elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) {
707
                // Cycle through all of the attributes in the node. Note that
708
                // these are DOMAttr objects.
709
                $matches = false;
710
                $name    = $attr['name'];
711
                foreach ($node->attributes as $attrNode) {
712
                    if ($attrNode->localName === $name) {
713
                        $nsuri   = $attrNode->namespaceURI;
714
                        $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']);
715
                    }
716
                }
717
            } // No namespace.
718
            else {
719
                $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']);
720
            }
721
722
            if (!$matches) {
723
                return false;
724
            }
725
        }
726
727
        return true;
728
    }
729
730
    /**
731
     * Check that the given DOMNode has the given ID.
732
     * @param \DOMElement $node
733
     * @param $id
734
     * @return bool
735
     */
736
    protected function matchId(\DOMElement $node, $id) : bool
737
    {
738
        if (empty($id)) {
739
            return true;
740
        }
741
742
        return $node->hasAttribute('id') && $node->getAttribute('id') === $id;
743
    }
744
745
    /**
746
     * Check that the given DOMNode has all of the given classes.
747
     * @param \DOMElement $node
748
     * @param $classes
749
     * @return bool
750
     */
751
    protected function matchClasses(\DOMElement $node, $classes) : bool
752
    {
753
        if (empty($classes)) {
754
            return true;
755
        }
756
757
        if (!$node->hasAttribute('class')) {
758
            return false;
759
        }
760
761
        $eleClasses = preg_split('/\s+/', $node->getAttribute('class'));
762
        if (empty($eleClasses)) {
763
            return false;
764
        }
765
766
        // The intersection should match the given $classes.
767
        $missing = array_diff($classes, array_intersect($classes, $eleClasses));
768
769
        return count($missing) === 0;
770
    }
771
772
    /**
773
     * @param \DOMElement $node
774
     * @param $pseudoClasses
775
     * @return bool
776
     * @throws NotImplementedException
777
     * @throws ParseException
778
     */
779
    protected function matchPseudoClasses(\DOMElement $node, $pseudoClasses): bool
780
    {
781
        $ret = true;
782
        foreach ($pseudoClasses as $pseudoClass) {
783
            $name = $pseudoClass['name'];
784
            // Avoid E_STRICT violation.
785
            $value = $pseudoClass['value'] ?? NULL;
786
            $ret   &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value);
0 ignored issues
show
Bug introduced by
$node of type DOMElement is incompatible with the type resource expected by parameter $node of QueryPath\CSS\DOMTravers...Class::elementMatches(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

786
            $ret   &= $this->psHandler->elementMatches($name, /** @scrutinizer ignore-type */ $node, $this->scopeNode, $value);
Loading history...
787
        }
788
789
        return $ret;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ret could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
790
    }
791
792
    /**
793
     * Test whether the given node matches the pseudoElements.
794
     *
795
     * If any pseudo-elements are passed, this will test to see
796
     * <i>if conditions obtain that would allow the pseudo-element
797
     * to be created</i>. This does not modify the match in any way.
798
     * @param \DOMElement $node
799
     * @param $pseudoElements
800
     * @return bool
801
     * @throws NotImplementedException
802
     */
803
    protected function matchPseudoElements(\DOMElement $node, $pseudoElements) : bool
804
    {
805
        if (empty($pseudoElements)) {
806
            return true;
807
        }
808
809
        foreach ($pseudoElements as $pse) {
810
            switch ($pse) {
811
                case 'first-line':
812
                case 'first-letter':
813
                case 'before':
814
                case 'after':
815
                    return strlen($node->textContent) > 0;
816
                case 'selection':
817
                    throw new \QueryPath\CSS\NotImplementedException("::$pse is not implemented.");
818
            }
819
        }
820
821
        return false;
822
    }
823
824
    protected function newMatches()
825
    {
826
        return new \SplObjectStorage();
827
    }
828
829
    /**
830
     * Get the internal match set.
831
     * Internal utility function.
832
     */
833
    protected function getMatches()
834
    {
835
        return $this->matches();
836
    }
837
838
    /**
839
     * Set the internal match set.
840
     *
841
     * Internal utility function.
842
     */
843
    protected function setMatches($matches)
844
    {
845
        $this->matches = $matches;
846
    }
847
848
    /**
849
     * Attach all nodes in a node list to the given \SplObjectStorage.
850
     * @param \DOMNodeList $nodeList
851
     * @param \SplObjectStorage $splos
852
     */
853
    public function attachNodeList(\DOMNodeList $nodeList, \SplObjectStorage $splos)
854
    {
855
        foreach ($nodeList as $item) {
856
            $splos->attach($item);
857
        }
858
    }
859
860
    public function getDocument()
861
    {
862
        return $this->dom;
863
    }
864
865
}
866