GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — 1.x ( 6c3e39...679511 )
by Christoph
05:26 queued 02:18
created

HtmlPageCrawler::wrap()   B

Complexity

Conditions 8
Paths 22

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8.0707

Importance

Changes 0
Metric Value
dl 0
loc 37
ccs 26
cts 29
cp 0.8966
rs 8.0835
c 0
b 0
f 0
cc 8
nc 22
nop 1
crap 8.0707
1
<?php
2
namespace Wa72\HtmlPageDom;
3
4
use Symfony\Component\DomCrawler\Crawler;
5
6
/**
7
 * Extends \Symfony\Component\DomCrawler\Crawler by adding tree manipulation functions
8
 * for HTML documents inspired by jQuery such as html(), css(), append(), prepend(), before(),
9
 * addClass(), removeClass()
10
 *
11
 * @author Christoph Singer
12
 * @license MIT
13
 *
14
 */
15
class HtmlPageCrawler extends Crawler
0 ignored issues
show
Complexity introduced by
This class has 1145 lines of code which exceeds the configured maximum of 1000.

Really long classes often contain too much logic and violate the single responsibility principle.

We suggest to take a look at the “Code” section for options on how to refactor this code.

Loading history...
Complexity introduced by
This class has 52 public methods and attributes which exceeds the configured maximum of 45.

The number of this metric differs depending on the chosen design (inheritance vs. composition). For inheritance, the number should generally be a bit lower.

A high number indicates a reusable class. It might also make the class harder to change without breaking other classes though.

Loading history...
Complexity introduced by
This class has a complexity of 166 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
16
{
17
    /**
18
     * the (internal) root element name used when importing html fragments
19
     * */
20
    const FRAGMENT_ROOT_TAGNAME = '_root';
21
22
    /**
23
     * Get an HtmlPageCrawler object from a HTML string, DOMNode, DOMNodeList or HtmlPageCrawler
24
     *
25
     * This is the equivalent to jQuery's $() function when used for wrapping DOMNodes or creating DOMElements from HTML code.
26
     *
27
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList|array $content
28
     * @return HtmlPageCrawler
29
     * @api
30
     */
31 19
    public static function create($content)
32
    {
33 19
        if ($content instanceof HtmlPageCrawler) {
34 3
            return $content;
35
        } else {
36 19
            return new HtmlPageCrawler($content);
37
        }
38
    }
39
40
    /**
41
     * Adds the specified class(es) to each element in the set of matched elements.
42
     *
43
     * @param string $name One or more space-separated classes to be added to the class attribute of each matched element.
44
     * @return HtmlPageCrawler $this for chaining
45
     * @api
46
     */
47 1
    public function addClass($name)
48
    {
49 1
        foreach ($this as $node) {
50 1
            if ($node instanceof \DOMElement) {
51
                /** @var \DOMElement $node */
52 1
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
53 1
                $found = false;
54 1
                $count = count($classes);
55 1
                for ($i = 0; $i < $count; $i++) {
56 1
                    if ($classes[$i] == $name) {
57 1
                        $found = true;
58 1
                    }
59 1
                }
60 1
                if (!$found) {
61 1
                    $classes[] = $name;
62 1
                    $node->setAttribute('class', trim(join(' ', $classes)));
63 1
                }
64 1
            }
65 1
        }
66 1
        return $this;
67
    }
68
69
    /**
70
     * Insert content, specified by the parameter, after each element in the set of matched elements.
71
     *
72
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
73
     * @return HtmlPageCrawler $this for chaining
74
     * @api
75
     */
76 3
    public function after($content)
77
    {
78 3
        $content = self::create($content);
79 3
        $newnodes = array();
80 3
        foreach ($this as $i => $node) {
81
            /** @var \DOMNode $node */
82 3
            $refnode = $node->nextSibling;
83 3
            foreach ($content as $newnode) {
84
                /** @var \DOMNode $newnode */
85 3
                $newnode = static::importNewnode($newnode, $node, $i);
86 3
                if ($refnode === null) {
87 3
                    $node->parentNode->appendChild($newnode);
88 3
                } else {
89 1
                    $node->parentNode->insertBefore($newnode, $refnode);
90
                }
91 3
                $newnodes[] = $newnode;
92 3
            }
93 3
        }
94 3
        $content->clear();
95 3
        $content->add($newnodes);
96 3
        return $this;
97
    }
98
99
    /**
100
     * Insert HTML content as child nodes of each element after existing children
101
     *
102
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment or DOMNode to append
103
     * @return HtmlPageCrawler $this for chaining
104
     * @api
105
     */
106 2
    public function append($content)
107
    {
108 2
        $content = self::create($content);
109 2
        $newnodes = array();
110 2
        foreach ($this as $i => $node) {
111
            /** @var \DOMNode $node */
112 2
            foreach ($content as $newnode) {
113
                /** @var \DOMNode $newnode */
114 2
                $newnode = static::importNewnode($newnode, $node, $i);
115
//                if ($newnode->ownerDocument !== $node->ownerDocument) {
116
//                    $newnode = $node->ownerDocument->importNode($newnode, true);
117
//                } else {
118
//                    if ($i > 0) {
119
//                        $newnode = $newnode->cloneNode(true);
120
//                    }
121
//                }
122 2
                $node->appendChild($newnode);
123 2
                $newnodes[] = $newnode;
124 2
            }
125 2
        }
126 2
        $content->clear();
127 2
        $content->add($newnodes);
128 2
        return $this;
129
    }
130
131
    /**
132
     * Insert every element in the set of matched elements to the end of the target.
133
     *
134
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
135
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
136
     * @api
137
     */
138 2
    public function appendTo($element)
139
    {
140 2
        $e = self::create($element);
141 2
        $newnodes = array();
142 2
        foreach ($e as $i => $node) {
143
            /** @var \DOMNode $node */
144 2
            foreach ($this as $newnode) {
145
                /** @var \DOMNode $newnode */
146 2
                if ($node !== $newnode) {
147 2
                    $newnode = static::importNewnode($newnode, $node, $i);
148 2
                    $node->appendChild($newnode);
149 2
                }
150 2
                $newnodes[] = $newnode;
151 2
            }
152 2
        }
153 2
        return self::create($newnodes);
154
    }
155
156
    /**
157
     * Returns the attribute value of the first node of the list, or sets an attribute on each element
158
     *
159
     * @see HtmlPageCrawler::getAttribute()
160
     * @see HtmlPageCrawler::setAttribute
161
     *
162
     * @param string $name
163
     * @param null|string $value
164
     * @return null|string|HtmlPageCrawler
165
     * @api
166
     */
167 2
    public function attr($name, $value = null)
168
    {
169 2
        if ($value === null) {
170 2
            return $this->getAttribute($name);
171
        } else {
172 1
            return $this->setAttribute($name, $value);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->setAttribute($name, $value); (Wa72\HtmlPageDom\HtmlPageCrawler) is incompatible with the return type of the parent method Symfony\Component\DomCrawler\Crawler::attr of type string|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
173
        }
174
    }
175
176
    /**
177
     * Sets an attribute on each element
178
     *
179
     * @param string $name
180
     * @param string $value
181
     * @return HtmlPageCrawler $this for chaining
182
     */
183 3
    public function setAttribute($name, $value)
184
    {
185 3
        foreach ($this as $node) {
186 3
            if ($node instanceof \DOMElement) {
187
                /** @var \DOMElement $node */
188 3
                $node->setAttribute($name, $value);
189 3
            }
190 3
        }
191 3
        return $this;
192
    }
193
194
    /**
195
     * Returns the attribute value of the first node of the list.
196
     *
197
     * @param string $name The attribute name
198
     * @return string|null The attribute value or null if the attribute does not exist
199
     * @throws \InvalidArgumentException When current node is empty
200
     *
201
     */
202 2
    public function getAttribute($name)
203
    {
204 2
        if (!count($this)) {
205 1
            throw new \InvalidArgumentException('The current node list is empty.');
206
        }
207 1
        $node = $this->getNode(0);
208 1
        return $node->hasAttribute($name) ? $node->getAttribute($name) : null;
209
    }
210
211
    /**
212
     * Insert content, specified by the parameter, before each element in the set of matched elements.
213
     *
214
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
215
     * @return HtmlPageCrawler $this for chaining
216
     * @api
217
     */
218 2
    public function before($content)
219
    {
220 2
        $content = self::create($content);
221 2
        $newnodes = array();
222 2
        foreach ($this as $i => $node) {
223
            /** @var \DOMNode $node */
224 2
            foreach ($content as $newnode) {
225
                /** @var \DOMNode $newnode */
226 2
                if ($node !== $newnode) {
227 2
                    $newnode = static::importNewnode($newnode, $node, $i);
228 2
                    $node->parentNode->insertBefore($newnode, $node);
229 2
                    $newnodes[] = $newnode;
230 2
                }
231 2
            }
232 2
        }
233 2
        $content->clear();
234 2
        $content->add($newnodes);
235 2
        return $this;
236
    }
237
238
    /**
239
     * Create a deep copy of the set of matched elements.
240
     *
241
     * Equivalent to clone() in jQuery (clone is not a valid PHP function name)
242
     *
243
     * @return HtmlPageCrawler
244
     * @api
245
     */
246 1
    public function makeClone()
247
    {
248 1
        return clone $this;
249
    }
250
251 1
    public function __clone()
252
    {
253 1
        $newnodes = array();
254 1
        foreach ($this as $node) {
255
            /** @var \DOMNode $node */
256 1
            $newnodes[] = $node->cloneNode(true);
257 1
        }
258 1
        $this->clear();
259 1
        $this->add($newnodes);
260 1
    }
261
262
    /**
263
     * Get one CSS style property of the first element or set it for all elements in the list
264
     *
265
     * Function is here for compatibility with jQuery; it is the same as getStyle() and setStyle()
266
     *
267
     * @see HtmlPageCrawler::getStyle()
268
     * @see HtmlPageCrawler::setStyle()
269
     *
270
     * @param string $key The name of the style property
271
     * @param null|string $value The CSS value to set, or NULL to get the current value
272
     * @return HtmlPageCrawler|string If no param is provided, returns the CSS styles of the first element
273
     * @api
274
     */
275 1
    public function css($key, $value = null)
276
    {
277 1
        if (null === $value) {
278 1
            return $this->getStyle($key);
279
        } else {
280 1
            return $this->setStyle($key, $value);
281
        }
282
    }
283
284
    /**
285
     * get one CSS style property of the first element
286
     *
287
     * @param string $key name of the property
288
     * @return string|null value of the property
289
     */
290 1
    public function getStyle($key)
291
    {
292 1
        $styles = Helpers::cssStringToArray($this->getAttribute('style'));
293 1
        return (isset($styles[$key]) ? $styles[$key] : null);
294
    }
295
296
    /**
297
     * set one CSS style property for all elements in the list
298
     *
299
     * @param string $key name of the property
300
     * @param string $value value of the property
301
     * @return HtmlPageCrawler $this for chaining
302
     */
303 1
    public function setStyle($key, $value)
304
    {
305 1
        foreach ($this as $node) {
306 1
            if ($node instanceof \DOMElement) {
307
                /** @var \DOMElement $node */
308 1
                $styles = Helpers::cssStringToArray($node->getAttribute('style'));
309 1
                if ($value != '') {
310 1
                    $styles[$key] = $value;
311 1
                } elseif (isset($styles[$key])) {
312 1
                    unset($styles[$key]);
313 1
                }
314 1
                $node->setAttribute('style', Helpers::cssArrayToString($styles));
315 1
            }
316 1
        }
317 1
        return $this;
318
    }
319
320
    /**
321
     * Removes all child nodes and text from all nodes in set
322
     *
323
     * Equivalent to jQuery's empty() function which is not a valid function name in PHP
324
     * @return HtmlPageCrawler $this
325
     * @api
326
     */
327 1
    public function makeEmpty()
328
    {
329 1
        foreach ($this as $node) {
330 1
            $node->nodeValue = '';
331 1
        }
332 1
        return $this;
333
    }
334
335
    /**
336
     * Determine whether any of the matched elements are assigned the given class.
337
     *
338
     * @param string $name
339
     * @return bool
340
     * @api
341
     */
342 2
    public function hasClass($name)
343
    {
344 2
        foreach ($this as $node) {
345 2
            if ($node instanceof \DOMElement && $class = $node->getAttribute('class')) {
346 2
                $classes = preg_split('/\s+/s', $class);
347 2
                if (in_array($name, $classes)) {
348 2
                    return true;
349
                }
350 1
            }
351 2
        }
352 2
        return false;
353
    }
354
355
    /**
356
     * Get the HTML contents of the first element in the set of matched elements
357
     * or set the HTML contents of every matched element.
358
     *
359
     * Deprecation warning: It will not be possible any more to use method html($html) as setter function in version 2.0. Use setInnerHtml($html) instead.
360
     *
361
     * @see HtmlPageCrawler::setInnerHtml()
362
     *
363
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList|null $html The HTML content to set, or NULL to get the current content
364
     *
365
     * @return HtmlPageCrawler|string If no param is provided, returns the HTML content of the first element
366
     * @api
367
     */
368 1
    public function html($html = null)
369
    {
370 1
        if (null === $html) {
371 1
            return $this->getInnerHtml();
0 ignored issues
show
Deprecated Code introduced by
The method Wa72\HtmlPageDom\HtmlPageCrawler::getInnerHtml() has been deprecated with message: Method is deprecated and will be removed in 2.0, use html() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
372
        } else {
373 1
            @trigger_error('It will not be possible any more to use method html($html) as setter function in version 2.0. Use setInnerHtml($html) instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
374 1
            $this->setInnerHtml($html);
375 1
            return $this;
376
        }
377
    }
378
379
    /**
380
     * Get the innerHTML contents of the first element
381
     *
382
     * @return string HTML code fragment
383
     * @deprecated Method is deprecated and will be removed in 2.0, use html() instead
384
     * @see html()
385
     */
386
    public function getInnerHtml()
387
    {
388
        @trigger_error('Method getInnerHtml() is deprecated and will be removed in 2.0, use html() instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
389
        $node = $this->getNode(0);
390
        if ($node instanceof \DOMNode) {
391
            $doc = new \DOMDocument('1.0', 'UTF-8');
392
            $doc->appendChild($doc->importNode($node, true));
393
            $html = trim($doc->saveHTML());
394
            $tag = $node->nodeName;
395
            return preg_replace('@^<' . $tag . '[^>]*>|</' . $tag . '>$@', '', $html);
396
        } else {
397
            return '';
398
        }
399
    }
400
401
    /**
402
     * Set the HTML contents of each element
403
     *
404
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
405
     * @return HtmlPageCrawler $this for chaining
406
     */
407 3
    public function setInnerHtml($content)
408
    {
409 3
        $content = self::create($content);
410 3
        foreach ($this as $node) {
411 3
            $node->nodeValue = '';
412 3
            foreach ($content as $newnode) {
413
                /** @var \DOMNode $node */
414
                /** @var \DOMNode $newnode */
415 3
                $newnode = static::importNewnode($newnode, $node);
416 3
                $node->appendChild($newnode);
417 3
            }
418 3
        }
419 3
        return $this;
420
    }
421
422
    /**
423
     * Insert every element in the set of matched elements after the target.
424
     *
425
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
426
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
427
     * @api
428
     */
429 2
    public function insertAfter($element)
430
    {
431 2
        $e = self::create($element);
432 2
        $newnodes = array();
433 2
        foreach ($e as $i => $node) {
434
            /** @var \DOMNode $node */
435 2
            $refnode = $node->nextSibling;
436 2
            foreach ($this as $newnode) {
437
                /** @var \DOMNode $newnode */
438 2
                $newnode = static::importNewnode($newnode, $node, $i);
439 2
                if ($refnode === null) {
440 2
                    $node->parentNode->appendChild($newnode);
441 2
                } else {
442 1
                    $node->parentNode->insertBefore($newnode, $refnode);
443
                }
444 2
                $newnodes[] = $newnode;
445 2
            }
446 2
        }
447 2
        return self::create($newnodes);
448
    }
449
450
    /**
451
     * Insert every element in the set of matched elements before the target.
452
     *
453
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
454
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
455
     * @api
456
     */
457 2
    public function insertBefore($element)
458
    {
459 2
        $e = self::create($element);
460 2
        $newnodes = array();
461 2
        foreach ($e as $i => $node) {
462
            /** @var \DOMNode $node */
463 2
            foreach ($this as $newnode) {
464
                /** @var \DOMNode $newnode */
465 2
                $newnode = static::importNewnode($newnode, $node, $i);
466 2
                if ($newnode !== $node) {
467 2
                    $node->parentNode->insertBefore($newnode, $node);
468 2
                }
469 2
                $newnodes[] = $newnode;
470 2
            }
471 2
        }
472 2
        return self::create($newnodes);
473
    }
474
475
    /**
476
     * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements.
477
     *
478
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
479
     * @return HtmlPageCrawler $this for chaining
480
     * @api
481
     */
482 2
    public function prepend($content)
483
    {
484 2
        $content = self::create($content);
485 2
        $newnodes = array();
486 2
        foreach ($this as $i => $node) {
487 2
            $refnode = $node->firstChild;
488
            /** @var \DOMNode $node */
489 2
            foreach ($content as $newnode) {
490
                /** @var \DOMNode $newnode */
491 2
                $newnode = static::importNewnode($newnode, $node, $i);
492 2
                if ($refnode === null) {
493 1
                    $node->appendChild($newnode);
494 2
                } else if ($refnode !== $newnode) {
495 2
                    $node->insertBefore($newnode, $refnode);
496 2
                }
497 2
                $newnodes[] = $newnode;
498 2
            }
499 2
        }
500 2
        $content->clear();
501 2
        $content->add($newnodes);
502 2
        return $this;
503
    }
504
505
    /**
506
     * Insert every element in the set of matched elements to the beginning of the target.
507
     *
508
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
509
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements prepended to the target elements
510
     * @api
511
     */
512 1
    public function prependTo($element)
513
    {
514 1
        $e = self::create($element);
515 1
        $newnodes = array();
516 1
        foreach ($e as $i => $node) {
517 1
            $refnode = $node->firstChild;
518
            /** @var \DOMNode $node */
519 1
            foreach ($this as $newnode) {
520
                /** @var \DOMNode $newnode */
521 1
                $newnode = static::importNewnode($newnode, $node, $i);
522 1
                if ($newnode !== $node) {
523 1
                    if ($refnode === null) {
524 1
                        $node->appendChild($newnode);
525 1
                    } else {
526 1
                        $node->insertBefore($newnode, $refnode);
527
                    }
528 1
                }
529 1
                $newnodes[] = $newnode;
530 1
            }
531 1
        }
532 1
        return self::create($newnodes);
533
    }
534
535
    /**
536
     * Remove the set of matched elements from the DOM.
537
     *
538
     * (as opposed to Crawler::clear() which detaches the nodes only from Crawler
539
     * but leaves them in the DOM)
540
     *
541
     * @api
542
     */
543 2
    public function remove()
544
    {
545 2
        foreach ($this as $node) {
546
            /**
547
             * @var \DOMNode $node
548
             */
549 2
            if ($node->parentNode instanceof \DOMElement) {
550 2
                $node->parentNode->removeChild($node);
551 2
            }
552 2
        }
553 2
        $this->clear();
554 2
    }
555
556
    /**
557
     * Remove an attribute from each element in the set of matched elements.
558
     *
559
     * Alias for removeAttribute for compatibility with jQuery
560
     *
561
     * @param string $name
562
     * @return HtmlPageCrawler
563
     * @api
564
     */
565 1
    public function removeAttr($name)
566
    {
567 1
        return $this->removeAttribute($name);
568
    }
569
570
    /**
571
     * Remove an attribute from each element in the set of matched elements.
572
     *
573
     * @param string $name
574
     * @return HtmlPageCrawler
575
     */
576 1
    public function removeAttribute($name)
577
    {
578 1
        foreach ($this as $node) {
579 1
            if ($node instanceof \DOMElement) {
580
                /** @var \DOMElement $node */
581 1
                if ($node->hasAttribute($name)) {
582 1
                    $node->removeAttribute($name);
583 1
                }
584 1
            }
585 1
        }
586 1
        return $this;
587
    }
588
589
    /**
590
     * Remove a class from each element in the list
591
     *
592
     * @param string $name
593
     * @return HtmlPageCrawler $this for chaining
594
     * @api
595
     */
596 2
    public function removeClass($name)
597
    {
598 2
        foreach ($this as $node) {
599 2
            if ($node instanceof \DOMElement) {
600
                /** @var \DOMElement $node */
601 2
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
602 2
                $count = count($classes);
603 2
                for ($i = 0; $i < $count; $i++) {
604 2
                    if ($classes[$i] == $name) {
605 2
                        unset($classes[$i]);
606 2
                    }
607 2
                }
608 2
                $node->setAttribute('class', trim(join(' ', $classes)));
609 2
            }
610 2
        }
611 2
        return $this;
612
    }
613
614
    /**
615
     * Replace each target element with the set of matched elements.
616
     *
617
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
618
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
619
     * @api
620
     */
621 2
    public function replaceAll($element)
622
    {
623 2
        $e = self::create($element);
624 2
        $newnodes = array();
625 2
        foreach ($e as $i => $node) {
626
            /** @var \DOMNode $node */
627 2
            $parent = $node->parentNode;
628 2
            $refnode  = $node->nextSibling;
629 2
            foreach ($this as $j => $newnode) {
630
                /** @var \DOMNode $newnode */
631 2
                $newnode = static::importNewnode($newnode, $node, $i);
632 2
                if ($j == 0) {
633 2
                    $parent->replaceChild($newnode, $node);
634 2
                } else {
635 1
                    $parent->insertBefore($newnode, $refnode);
636
                }
637 2
                $newnodes[] = $newnode;
638 2
            }
639 2
        }
640 2
        return self::create($newnodes);
641
    }
642
643
    /**
644
     * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed.
645
     *
646
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
647
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
648
     * @api
649
     */
650 2
    public function replaceWith($content)
651
    {
652 2
        $content = self::create($content);
653 2
        $newnodes = array();
654 2
        foreach ($this as $i => $node) {
655
            /** @var \DOMNode $node */
656 2
            $parent = $node->parentNode;
657 2
            $refnode  = $node->nextSibling;
658 2
            foreach ($content as $j => $newnode) {
659
                /** @var \DOMNode $newnode */
660 2
                $newnode = static::importNewnode($newnode, $node, $i);
661 2
                if ($j == 0) {
662 2
                    $parent->replaceChild($newnode, $node);
663 2
                } else {
664 1
                    $parent->insertBefore($newnode, $refnode);
665
                }
666 2
                $newnodes[] = $newnode;
667 2
            }
668 2
        }
669 2
        $content->clear();
670 2
        $content->add($newnodes);
671 2
        return $this;
672
    }
673
674
    /**
675
     * Get the combined text contents of each element in the set of matched elements, including their descendants,
676
     * or set the text contents of the matched elements.
677
     *
678
     * ATTENTION: Contrary to the parent Crawler class, which returns the text from the first element only,
679
     * this functions returns the combined text of all elements (as jQuery does). If this is not what you need you
680
     * must call ->first() before calling ->text(), e.g.
681
     *
682
     * in Symfony\DOMCrawler\Crawler: $c->filter('p')->text() returns the text of the first paragraph only
683
     * in HtmlPageCrawler you need to call: $c->filter('p')->first()->text()
684
     *
685
     * DEPRECATION WARNING:
686
     * This function will be removed from here in 2.0, so calling text() then will call the parent implementation, i.e.
687
     * it will return the text from the first node only, and an argument passed is treated as default value for the
688
     * getter function from Symfony 4.3 onwards. It will not be possible to use this function as setter.
689
     *
690
     * Use getCombinedText() for a getter with the old behavior, and use setText() for setting text content.
691
     *
692
     * @see setText()
693
     * @see getCombinedText()
694
     *
695
     * @param null|string $text
696
     * @return string|HtmlPageCrawler
697
     * @api
698
     */
699 2
    public function text($text = null)
700
    {
701 2
        if ($text === null) {
702 2
            @trigger_error('In Version 2.0, Method text() will return the text from only the first element in the set. Consider using getCombinedText() instead.', E_USER_NOTICE);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
703 2
            return $this->getCombinedText();
704
        } else {
705 1
            @trigger_error('It will not be possible any more to use method text($text) as setter function in version 2.0. Use setText($text) instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
706 1
            return $this->setText($text);
707
        }
708
    }
709
710
    /**
711
     * Get the combined text contents of each element in the set of matched elements, including their descendants.
712
     * This is what the jQuery text() function does, contrary to the Crawler::text() method that returns only
713
     * the text of the first node.
714
     *
715
     * @return string
716
     * @api
717
     * @since 1.4
718
     */
719 2
    public function getCombinedText()
720
    {
721 2
        $text = '';
722 2
        foreach ($this as $node) {
723
            /** @var \DOMNode $node */
724 2
            $text .= $node->nodeValue;
725 2
        }
726 2
        return $text;
727
    }
728
    /**
729
     * Set the text contents of the matched elements.
730
     *
731
     * @param string $text
732
     * @return HtmlPageCrawler
733
     * @api
734
     * @since 1.4
735
     */
736 2
    public function setText($text)
737
    {
738 2
        $text = htmlspecialchars($text);
739 2
        foreach ($this as $node) {
740
            /** @var \DOMNode $node */
741 2
            $node->nodeValue = $text;
742 2
        }
743 2
        return $this;
744
    }
745
746
747
    /**
748
     * Add or remove one or more classes from each element in the set of matched elements, depending the class’s presence.
749
     *
750
     * @param string $classname One or more classnames separated by spaces
751
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
752
     * @api
753
     */
754 1
    public function toggleClass($classname)
755
    {
756 1
        $classes = explode(' ', $classname);
757 1
        foreach ($this as $i => $node) {
758 1
            $c = self::create($node);
759
            /** @var \DOMNode $node */
760 1
            foreach ($classes as $class) {
761 1
                if ($c->hasClass($class)) {
762 1
                    $c->removeClass($class);
763 1
                } else {
764 1
                    $c->addClass($class);
765
                }
766 1
            }
767 1
        }
768 1
        return $this;
769
    }
770
771
    /**
772
     * Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
773
     *
774
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
775
     * @api
776
     */
777 1
    public function unwrap()
778
    {
779 1
        $parents = array();
780 1
        foreach($this as $i => $node) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
781 1
            $parents[] = $node->parentNode;
782 1
        }
783
784 1
        self::create($parents)->unwrapInner();
785 1
        return $this;
786
    }
787
788
    /**
789
     * Remove the matched elements, but promote the children to take their place.
790
     *
791
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
792
     * @api
793
     */
794 2
    public function unwrapInner()
795
    {
796 2
        foreach($this as $i => $node) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
797 2
            if (!$node->parentNode instanceof \DOMElement) {
798 1
                throw new \InvalidArgumentException('DOMElement does not have a parent DOMElement node.');
799
            }
800
801
            /** @var \DOMNode[] $children */
802 2
            $children = iterator_to_array($node->childNodes);
803 2
            foreach ($children as $child) {
804 1
                $node->parentNode->insertBefore($child, $node);
805 2
            }
806
807 2
            $node->parentNode->removeChild($node);
808 2
        }
809 2
    }
810
811
812
    /**
813
     * Wrap an HTML structure around each element in the set of matched elements
814
     *
815
     * The HTML structure must contain only one root node, e.g.:
816
     * Works: <div><div></div></div>
817
     * Does not work: <div></div><div></div>
818
     *
819
     * @param string|HtmlPageCrawler|\DOMNode $wrappingElement
820
     * @return HtmlPageCrawler $this for chaining
821
     * @api
822
     */
823 1
    public function wrap($wrappingElement)
824
    {
825 1
        $content = self::create($wrappingElement);
826 1
        $newnodes = array();
827 1
        foreach ($this as $i => $node) {
828
            /** @var \DOMNode $node */
829 1
            $newnode = $content->getNode(0);
830
            /** @var \DOMNode $newnode */
831
//            $newnode = static::importNewnode($newnode, $node, $i);
832 1
            if ($newnode->ownerDocument !== $node->ownerDocument) {
833 1
                $newnode = $node->ownerDocument->importNode($newnode, true);
834 1
            } else {
835
                if ($i > 0) {
836
                    $newnode = $newnode->cloneNode(true);
837
                }
838
            }
839 1
            $oldnode = $node->parentNode->replaceChild($newnode, $node);
840 1
            while ($newnode->hasChildNodes()) {
841 1
                $elementFound = false;
842 1
                foreach ($newnode->childNodes as $child) {
843 1
                    if ($child instanceof \DOMElement) {
844 1
                        $newnode = $child;
845 1
                        $elementFound = true;
846 1
                        break;
847
                    }
848 1
                }
849 1
                if (!$elementFound) {
850 1
                    break;
851
                }
852 1
            }
853 1
            $newnode->appendChild($oldnode);
854 1
            $newnodes[] = $newnode;
855 1
        }
856 1
        $content->clear();
857 1
        $content->add($newnodes);
858 1
        return $this;
859
    }
860
861
    /**
862
     * Wrap an HTML structure around all elements in the set of matched elements.
863
     *
864
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
865
     * @throws \LogicException
866
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
867
     * @api
868
     */
869 1
    public function wrapAll($content)
870
    {
871 1
        $content = self::create($content);
872 1
        $parent = $this->getNode(0)->parentNode;
873 1
        foreach ($this as $i => $node) {
874
            /** @var \DOMNode $node */
875 1
            if ($node->parentNode !== $parent) {
876
                throw new \LogicException('Nodes to be wrapped with wrapAll() must all have the same parent');
877
            }
878 1
        }
879
880 1
        $newnode = $content->getNode(0);
881
        /** @var \DOMNode $newnode */
882 1
        $newnode = static::importNewnode($newnode, $parent);
883
884 1
        $newnode = $parent->insertBefore($newnode,$this->getNode(0));
885 1
        $content->clear();
886 1
        $content->add($newnode);
887
888 1
        while ($newnode->hasChildNodes()) {
889 1
            $elementFound = false;
890 1
            foreach ($newnode->childNodes as $child) {
891 1
                if ($child instanceof \DOMElement) {
892 1
                    $newnode = $child;
893 1
                    $elementFound = true;
894 1
                    break;
895
                }
896 1
            }
897 1
            if (!$elementFound) {
898
                break;
899
            }
900 1
        }
901 1
        foreach ($this as $i => $node) {
902
            /** @var \DOMNode $node */
903 1
            $newnode->appendChild($node);
904 1
        }
905 1
        return $this;
906
    }
907
908
    /**
909
     * Wrap an HTML structure around the content of each element in the set of matched elements.
910
     *
911
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
912
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
913
     * @api
914
     */
915 1
    public function wrapInner($content)
916
    {
917 1
        foreach ($this as $i => $node) {
918
            /** @var \DOMNode $node */
919 1
            self::create($node->childNodes)->wrapAll($content);
920 1
        }
921 1
        return $this;
922
    }
923
924
    /**
925
     * Get the HTML code fragment of all elements and their contents.
926
     *
927
     * If the first node contains a complete HTML document return only
928
     * the full code of this document.
929
     *
930
     * @return string HTML code (fragment)
931
     * @api
932
     */
933 8
    public function saveHTML()
934
    {
935 8
        if ($this->isHtmlDocument()) {
936 1
            return $this->getDOMDocument()->saveHTML();
937
        } else {
938 8
            $doc = new \DOMDocument('1.0', 'UTF-8');
939 8
            $root = $doc->appendChild($doc->createElement('_root'));
940 8
            foreach ($this as $node) {
941 8
                $root->appendChild($doc->importNode($node, true));
942 8
            }
943 8
            $html = trim($doc->saveHTML());
944 8
            return preg_replace('@^<'.self::FRAGMENT_ROOT_TAGNAME.'[^>]*>|</'.self::FRAGMENT_ROOT_TAGNAME.'>$@', '', $html);
945
        }
946
    }
947
948 4
    public function __toString()
949
    {
950 4
        return $this->saveHTML();
951
    }
952
953
    /**
954
     * checks whether the first node contains a complete html document
955
     * (as opposed to a document fragment)
956
     *
957
     * @return boolean
958
     */
959 8
    public function isHtmlDocument()
960
    {
961 8
        $node = $this->getNode(0);
962
        if ($node instanceof \DOMElement
963 8
            && $node->ownerDocument instanceof \DOMDocument
964 8
            && $node->ownerDocument->documentElement === $node
965 8
            && $node->nodeName == 'html'
966 8
        ) {
967 1
            return true;
968
        } else {
969 8
            return false;
970
        }
971
    }
972
973
    /**
974
     * get ownerDocument of the first element
975
     *
976
     * @return \DOMDocument|null
977
     */
978 1
    public function getDOMDocument()
979
    {
980 1
        $node = $this->getNode(0);
981 1
        $r = null;
982
        if ($node instanceof \DOMElement
983 1
            && $node->ownerDocument instanceof \DOMDocument
984 1
        ) {
985 1
            $r = $node->ownerDocument;
986 1
        }
987 1
        return $r;
988
    }
989
990
    /**
991
     * Filters the list of nodes with a CSS selector.
992
     *
993
     * @param string $selector
994
     * @return HtmlPageCrawler
995
     */
996 8
    public function filter($selector)
997
    {
998 8
        return parent::filter($selector);
999
    }
1000
1001
    /**
1002
     * Filters the list of nodes with an XPath expression.
1003
     *
1004
     * @param string $xpath An XPath expression
1005
     *
1006
     * @return HtmlPageCrawler A new instance of Crawler with the filtered list of nodes
1007
     *
1008
     * @api
1009
     */
1010 2
    public function filterXPath($xpath)
1011
    {
1012 2
        return parent::filterXPath($xpath);
1013
    }
1014
1015
    /**
1016
     * Adds HTML/XML content to the HtmlPageCrawler object (but not to the DOM of an already attached node).
1017
     *
1018
     * Function overriden from Crawler because HTML fragments are always added as complete documents there
1019
     *
1020
     *
1021
     * @param string      $content A string to parse as HTML/XML
1022
     * @param null|string $type    The content type of the string
1023
     *
1024
     * @return null|void
1025
     */
1026 18
    public function addContent($content, $type = null)
1027
    {
1028 18
        if (empty($type)) {
1029 18
            $type = 'text/html;charset=UTF-8';
1030 18
        }
1031 18
        if (substr($type, 0, 9) == 'text/html' && !preg_match('/<html\b[^>]*>/i', $content)) {
1032
            // string contains no <html> Tag => no complete document but an HTML fragment!
1033 17
            $this->addHtmlFragment($content);
1034 17
        } else {
1035 2
            parent::addContent($content, $type);
1036
        }
1037 18
    }
1038
1039 16
    public function addHtmlFragment($content, $charset = 'UTF-8')
1040
    {
1041 16
        $d = new \DOMDocument('1.0', $charset);
1042 16
        $root = $d->appendChild($d->createElement(self::FRAGMENT_ROOT_TAGNAME));
1043 16
        $bodynode = Helpers::getBodyNodeFromHtmlFragment($content, $charset);
1044 16
        foreach ($bodynode->childNodes as $child) {
1045 16
            $inode = $root->appendChild($d->importNode($child, true));
1046 16
            if ($inode) {
1047 16
                $this->addNode($inode);
1048 16
            }
1049 16
        }
1050 16
    }
1051
1052
    /**
1053
     * returns the first node
1054
     * deprecated, use getNode(0) instead
1055
     *
1056
     * @return \DOMNode|null
1057
     * @deprecated
1058
     * @see Crawler::getNode
1059
     */
1060
    public function getFirstNode()
1061
    {
1062
        return $this->getNode(0);
1063
    }
1064
1065
    /**
1066
     * @param int $position
1067
     *
1068
     * overridden from Crawler because it is not public in Symfony 2.3
1069
     * TODO: throw away as soon as we don't need to support SF 2.3 any more
1070
     *
1071
     * @return \DOMElement|null
1072
     */
1073 14
    public function getNode($position)
1074
    {
1075 14
        return parent::getNode($position);
1076
    }
1077
1078
    /**
1079
     * Returns the node name of the first node of the list.
1080
     *
1081
     * in Crawler (parent), this function will be available starting with 2.6.0,
1082
     * therefore this method be removed from here as soon as we don't need to keep compatibility
1083
     * with Symfony < 2.6
1084
     *
1085
     * TODO: throw away as soon as we don't need to support SF 2.3 any more
1086
     *
1087
     * @return string The node name
1088
     *
1089
     * @throws \InvalidArgumentException When current node is empty
1090
     */
1091 1
    public function nodeName()
1092
    {
1093 1
        if (!count($this)) {
1094
            throw new \InvalidArgumentException('The current node list is empty.');
1095
        }
1096 1
        return $this->getNode(0)->nodeName;
1097
    }
1098
1099
    /**
1100
     * Adds a node to the current list of nodes.
1101
     *
1102
     * This method uses the appropriate specialized add*() method based
1103
     * on the type of the argument.
1104
     *
1105
     * Overwritten from parent to allow Crawler to be added
1106
     *
1107
     * @param null|\DOMNodeList|array|\DOMNode|Crawler $node A node
1108
     *
1109
     * @api
1110
     */
1111 30
    public function add($node)
1112
    {
1113 30
        if ($node instanceof Crawler) {
1114 1
            foreach ($node as $childnode) {
1115 1
                $this->addNode($childnode);
1116 1
            }
1117 1
        } else {
1118 30
            parent::add($node);
1119
        }
1120 30
    }
1121
1122
    /**
1123
     * @param \DOMNode $newnode
1124
     * @param \DOMNode $referencenode
1125
     * @param int $clone
1126
     * @return \DOMNode
1127
     */
1128 6
    protected static function importNewnode(\DOMNode $newnode, \DOMNode $referencenode, $clone = 0) {
1129 6
        if ($newnode->ownerDocument !== $referencenode->ownerDocument) {
1130 5
            $newnode = $referencenode->ownerDocument->importNode($newnode, true);
1131 5
        } else {
1132 2
            if ($clone > 0) {
1133
                $newnode = $newnode->cloneNode(true);
1134
            }
1135
        }
1136 6
        return $newnode;
1137
    }
1138
1139
    /**
1140
     * Checks whether the first node in the set is disconnected (has no parent node)
1141
     *
1142
     * @return bool
1143
     */
1144 1
    public function isDisconnected()
1145
    {
1146 1
        $parent = $this->getNode(0)->parentNode;
1147 1
        return ($parent == null || $parent->tagName == self::FRAGMENT_ROOT_TAGNAME);
1148
    }
1149
1150 1
    public function __get($name)
1151
    {
1152
        switch ($name) {
1153 1
            case 'count':
1154 1
            case 'length':
1155 1
                return count($this);
1156
        }
1157 1
        throw new \Exception('No such property ' . $name);
1158
    }
1159
}
1160