One Hat Cyber Team
Your IP:
216.73.216.101
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
proc
/
thread-self
/
root
/
proc
/
thread-self
/
cwd
/
View File Name :
dom-crawler.tar
Form.php 0000644 00000036061 15112032667 0006171 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; use Symfony\Component\DomCrawler\Field\ChoiceFormField; use Symfony\Component\DomCrawler\Field\FormField; /** * Form represents an HTML form. * * @author Fabien Potencier <fabien@symfony.com> */ class Form extends Link implements \ArrayAccess { private \DOMElement $button; private FormFieldRegistry $fields; private ?string $baseHref; /** * @param \DOMElement $node A \DOMElement instance * @param string|null $currentUri The URI of the page where the form is embedded * @param string|null $method The method to use for the link (if null, it defaults to the method defined by the form) * @param string|null $baseHref The URI of the <base> used for relative links, but not for empty action * * @throws \LogicException if the node is not a button inside a form tag */ public function __construct(\DOMElement $node, string $currentUri = null, string $method = null, string $baseHref = null) { parent::__construct($node, $currentUri, $method); $this->baseHref = $baseHref; $this->initialize(); } /** * Gets the form node associated with this form. */ public function getFormNode(): \DOMElement { return $this->node; } /** * Sets the value of the fields. * * @param array $values An array of field values * * @return $this */ public function setValues(array $values): static { foreach ($values as $name => $value) { $this->fields->set($name, $value); } return $this; } /** * Gets the field values. * * The returned array does not include file fields (@see getFiles). */ public function getValues(): array { $values = []; foreach ($this->fields->all() as $name => $field) { if ($field->isDisabled()) { continue; } if (!$field instanceof Field\FileFormField && $field->hasValue()) { $values[$name] = $field->getValue(); } } return $values; } /** * Gets the file field values. */ public function getFiles(): array { if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) { return []; } $files = []; foreach ($this->fields->all() as $name => $field) { if ($field->isDisabled()) { continue; } if ($field instanceof Field\FileFormField) { $files[$name] = $field->getValue(); } } return $files; } /** * Gets the field values as PHP. * * This method converts fields with the array notation * (like foo[bar] to arrays) like PHP does. */ public function getPhpValues(): array { $values = []; foreach ($this->getValues() as $name => $value) { $qs = http_build_query([$name => $value], '', '&'); if (!empty($qs)) { parse_str($qs, $expandedValue); $varName = substr($name, 0, \strlen(key($expandedValue))); $values[] = [$varName => current($expandedValue)]; } } return array_replace_recursive([], ...$values); } /** * Gets the file field values as PHP. * * This method converts fields with the array notation * (like foo[bar] to arrays) like PHP does. * The returned array is consistent with the array for field values * (@see getPhpValues), rather than uploaded files found in $_FILES. * For a compound file field foo[bar] it will create foo[bar][name], * instead of foo[name][bar] which would be found in $_FILES. */ public function getPhpFiles(): array { $values = []; foreach ($this->getFiles() as $name => $value) { $qs = http_build_query([$name => $value], '', '&'); if (!empty($qs)) { parse_str($qs, $expandedValue); $varName = substr($name, 0, \strlen(key($expandedValue))); array_walk_recursive( $expandedValue, function (&$value, $key) { if (ctype_digit($value) && ('size' === $key || 'error' === $key)) { $value = (int) $value; } } ); reset($expandedValue); $values[] = [$varName => current($expandedValue)]; } } return array_replace_recursive([], ...$values); } /** * Gets the URI of the form. * * The returned URI is not the same as the form "action" attribute. * This method merges the value if the method is GET to mimics * browser behavior. */ public function getUri(): string { $uri = parent::getUri(); if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) { $query = parse_url($uri, \PHP_URL_QUERY); $currentParameters = []; if ($query) { parse_str($query, $currentParameters); } $queryString = http_build_query(array_merge($currentParameters, $this->getValues()), '', '&'); $pos = strpos($uri, '?'); $base = false === $pos ? $uri : substr($uri, 0, $pos); $uri = rtrim($base.'?'.$queryString, '?'); } return $uri; } protected function getRawUri(): string { // If the form was created from a button rather than the form node, check for HTML5 action overrides if ($this->button !== $this->node && $this->button->getAttribute('formaction')) { return $this->button->getAttribute('formaction'); } return $this->node->getAttribute('action'); } /** * Gets the form method. * * If no method is defined in the form, GET is returned. */ public function getMethod(): string { if (null !== $this->method) { return $this->method; } // If the form was created from a button rather than the form node, check for HTML5 method override if ($this->button !== $this->node && $this->button->getAttribute('formmethod')) { return strtoupper($this->button->getAttribute('formmethod')); } return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET'; } /** * Gets the form name. * * If no name is defined on the form, an empty string is returned. */ public function getName(): string { return $this->node->getAttribute('name'); } /** * Returns true if the named field exists. */ public function has(string $name): bool { return $this->fields->has($name); } /** * Removes a field from the form. * * @return void */ public function remove(string $name) { $this->fields->remove($name); } /** * Gets a named field. * * @return FormField|FormField[]|FormField[][] * * @throws \InvalidArgumentException When field is not present in this form */ public function get(string $name): FormField|array { return $this->fields->get($name); } /** * Sets a named field. * * @return void */ public function set(FormField $field) { $this->fields->add($field); } /** * Gets all fields. * * @return FormField[] */ public function all(): array { return $this->fields->all(); } /** * Returns true if the named field exists. * * @param string $name The field name */ public function offsetExists(mixed $name): bool { return $this->has($name); } /** * Gets the value of a field. * * @param string $name The field name * * @return FormField|FormField[]|FormField[][] * * @throws \InvalidArgumentException if the field does not exist */ public function offsetGet(mixed $name): FormField|array { return $this->fields->get($name); } /** * Sets the value of a field. * * @param string $name The field name * @param string|array $value The value of the field * * @throws \InvalidArgumentException if the field does not exist */ public function offsetSet(mixed $name, mixed $value): void { $this->fields->set($name, $value); } /** * Removes a field from the form. * * @param string $name The field name */ public function offsetUnset(mixed $name): void { $this->fields->remove($name); } /** * Disables validation. * * @return $this */ public function disableValidation(): static { foreach ($this->fields->all() as $field) { if ($field instanceof Field\ChoiceFormField) { $field->disableValidation(); } } return $this; } /** * Sets the node for the form. * * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself. * * @return void * * @throws \LogicException If given node is not a button or input or does not have a form ancestor */ protected function setNode(\DOMElement $node) { $this->button = $node; if ('button' === $node->nodeName || ('input' === $node->nodeName && \in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image']))) { if ($node->hasAttribute('form')) { // if the node has the HTML5-compliant 'form' attribute, use it $formId = $node->getAttribute('form'); $form = $node->ownerDocument->getElementById($formId); if (null === $form) { throw new \LogicException(sprintf('The selected node has an invalid form attribute (%s).', $formId)); } $this->node = $form; return; } // we loop until we find a form ancestor do { if (null === $node = $node->parentNode) { throw new \LogicException('The selected node does not have a form ancestor.'); } } while ('form' !== $node->nodeName); } elseif ('form' !== $node->nodeName) { throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName)); } $this->node = $node; } /** * Adds form elements related to this form. * * Creates an internal copy of the submitted 'button' element and * the form node or the entire document depending on whether we need * to find non-descendant elements through HTML5 'form' attribute. */ private function initialize(): void { $this->fields = new FormFieldRegistry(); $xpath = new \DOMXPath($this->node->ownerDocument); // add submitted button if it has a valid name if ('form' !== $this->button->nodeName && $this->button->hasAttribute('name') && $this->button->getAttribute('name')) { if ('input' == $this->button->nodeName && 'image' == strtolower($this->button->getAttribute('type'))) { $name = $this->button->getAttribute('name'); $this->button->setAttribute('value', '0'); // temporarily change the name of the input node for the x coordinate $this->button->setAttribute('name', $name.'.x'); $this->set(new Field\InputFormField($this->button)); // temporarily change the name of the input node for the y coordinate $this->button->setAttribute('name', $name.'.y'); $this->set(new Field\InputFormField($this->button)); // restore the original name of the input node $this->button->setAttribute('name', $name); } else { $this->set(new Field\InputFormField($this->button)); } } // find form elements corresponding to the current form if ($this->node->hasAttribute('id')) { // corresponding elements are either descendants or have a matching HTML5 form attribute $formId = Crawler::xpathLiteral($this->node->getAttribute('id')); $fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[not(ancestor::template)]', $formId)); foreach ($fieldNodes as $node) { $this->addField($node); } } else { // do the xpath query with $this->node as the context node, to only find descendant elements // however, descendant elements with form attribute are not part of this form $fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[not(ancestor::template)]', $this->node); foreach ($fieldNodes as $node) { $this->addField($node); } } if ($this->baseHref && '' !== $this->node->getAttribute('action')) { $this->currentUri = $this->baseHref; } } private function addField(\DOMElement $node): void { if (!$node->hasAttribute('name') || !$node->getAttribute('name')) { return; } $nodeName = $node->nodeName; if ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == strtolower($node->getAttribute('type'))) { $this->set(new Field\ChoiceFormField($node)); } elseif ('input' == $nodeName && 'radio' == strtolower($node->getAttribute('type'))) { // there may be other fields with the same name that are no choice // fields already registered (see https://github.com/symfony/symfony/issues/11689) if ($this->has($node->getAttribute('name')) && $this->get($node->getAttribute('name')) instanceof ChoiceFormField) { $this->get($node->getAttribute('name'))->addChoice($node); } else { $this->set(new Field\ChoiceFormField($node)); } } elseif ('input' == $nodeName && 'file' == strtolower($node->getAttribute('type'))) { $this->set(new Field\FileFormField($node)); } elseif ('input' == $nodeName && !\in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image'])) { $this->set(new Field\InputFormField($node)); } elseif ('textarea' == $nodeName) { $this->set(new Field\TextareaFormField($node)); } } } README.md 0000644 00000000762 15112032670 0006025 0 ustar 00 DomCrawler Component ==================== The DomCrawler component eases DOM navigation for HTML and XML documents. Resources --------- * [Documentation](https://symfony.com/doc/current/components/dom_crawler.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) UriResolver.php 0000644 00000007005 15112032670 0007535 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; /** * The UriResolver class takes an URI (relative, absolute, fragment, etc.) * and turns it into an absolute URI against another given base URI. * * @author Fabien Potencier <fabien@symfony.com> * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class UriResolver { /** * Resolves a URI according to a base URI. * * For example if $uri=/foo/bar and $baseUri=https://symfony.com it will * return https://symfony.com/foo/bar * * If the $uri is not absolute you must pass an absolute $baseUri */ public static function resolve(string $uri, ?string $baseUri): string { $uri = trim($uri); // absolute URL? if (null !== parse_url($uri, \PHP_URL_SCHEME)) { return $uri; } if (null === $baseUri) { throw new \InvalidArgumentException('The URI is relative, so you must define its base URI passing an absolute URL.'); } // empty URI if (!$uri) { return $baseUri; } // an anchor if ('#' === $uri[0]) { return self::cleanupAnchor($baseUri).$uri; } $baseUriCleaned = self::cleanupUri($baseUri); if ('?' === $uri[0]) { return $baseUriCleaned.$uri; } // absolute URL with relative schema if (str_starts_with($uri, '//')) { return preg_replace('#^([^/]*)//.*$#', '$1', $baseUriCleaned).$uri; } $baseUriCleaned = preg_replace('#^(.*?//[^/]*)(?:\/.*)?$#', '$1', $baseUriCleaned); // absolute path if ('/' === $uri[0]) { return $baseUriCleaned.$uri; } // relative path $path = parse_url(substr($baseUri, \strlen($baseUriCleaned)), \PHP_URL_PATH) ?? ''; $path = self::canonicalizePath(substr($path, 0, strrpos($path, '/')).'/'.$uri); return $baseUriCleaned.('' === $path || '/' !== $path[0] ? '/' : '').$path; } /** * Returns the canonicalized URI path (see RFC 3986, section 5.2.4). */ private static function canonicalizePath(string $path): string { if ('' === $path || '/' === $path) { return $path; } if (str_ends_with($path, '.')) { $path .= '/'; } $output = []; foreach (explode('/', $path) as $segment) { if ('..' === $segment) { array_pop($output); } elseif ('.' !== $segment) { $output[] = $segment; } } return implode('/', $output); } /** * Removes the query string and the anchor from the given uri. */ private static function cleanupUri(string $uri): string { return self::cleanupQuery(self::cleanupAnchor($uri)); } /** * Removes the query string from the uri. */ private static function cleanupQuery(string $uri): string { if (false !== $pos = strpos($uri, '?')) { return substr($uri, 0, $pos); } return $uri; } /** * Removes the anchor from the uri. */ private static function cleanupAnchor(string $uri): string { if (false !== $pos = strpos($uri, '#')) { return substr($uri, 0, $pos); } return $uri; } } LICENSE 0000644 00000002054 15112032670 0005547 0 ustar 00 Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. composer.json 0000644 00000001567 15112032670 0007274 0 ustar 00 { "name": "symfony/dom-crawler", "type": "library", "description": "Eases DOM navigation for HTML and XML documents", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "masterminds/html5": "^2.6" }, "require-dev": { "symfony/css-selector": "^5.4|^6.0" }, "autoload": { "psr-4": { "Symfony\\Component\\DomCrawler\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } AbstractUriElement.php 0000644 00000006453 15112032670 0011017 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; /** * Any HTML element that can link to an URI. * * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractUriElement { /** * @var \DOMElement */ protected $node; /** * @var string|null The method to use for the element */ protected $method; /** * @var string The URI of the page where the element is embedded (or the base href) */ protected $currentUri; /** * @param \DOMElement $node A \DOMElement instance * @param string|null $currentUri The URI of the page where the link is embedded (or the base href) * @param string|null $method The method to use for the link (GET by default) * * @throws \InvalidArgumentException if the node is not a link */ public function __construct(\DOMElement $node, string $currentUri = null, ?string $method = 'GET') { $this->setNode($node); $this->method = $method ? strtoupper($method) : null; $this->currentUri = $currentUri; $elementUriIsRelative = null === parse_url(trim($this->getRawUri()), \PHP_URL_SCHEME); $baseUriIsAbsolute = null !== $this->currentUri && \in_array(strtolower(substr($this->currentUri, 0, 4)), ['http', 'file']); if ($elementUriIsRelative && !$baseUriIsAbsolute) { throw new \InvalidArgumentException(sprintf('The URL of the element is relative, so you must define its base URI passing an absolute URL to the constructor of the "%s" class ("%s" was passed).', __CLASS__, $this->currentUri)); } } /** * Gets the node associated with this link. */ public function getNode(): \DOMElement { return $this->node; } /** * Gets the method associated with this link. */ public function getMethod(): string { return $this->method ?? 'GET'; } /** * Gets the URI associated with this link. */ public function getUri(): string { return UriResolver::resolve($this->getRawUri(), $this->currentUri); } /** * Returns raw URI data. */ abstract protected function getRawUri(): string; /** * Returns the canonicalized URI path (see RFC 3986, section 5.2.4). * * @param string $path URI path */ protected function canonicalizePath(string $path): string { if ('' === $path || '/' === $path) { return $path; } if (str_ends_with($path, '.')) { $path .= '/'; } $output = []; foreach (explode('/', $path) as $segment) { if ('..' === $segment) { array_pop($output); } elseif ('.' !== $segment) { $output[] = $segment; } } return implode('/', $output); } /** * Sets current \DOMElement instance. * * @param \DOMElement $node A \DOMElement instance * * @return void * * @throws \LogicException If given node is not an anchor */ abstract protected function setNode(\DOMElement $node); } error_log 0000644 00000007334 15112032670 0006465 0 ustar 00 [19-Nov-2025 13:19:03 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php on line 17 [19-Nov-2025 17:45:58 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php on line 19 [19-Nov-2025 18:09:42 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Link" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php on line 22 [19-Nov-2025 19:43:20 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php on line 17 [20-Nov-2025 00:18:51 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php on line 19 [20-Nov-2025 00:49:00 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Link" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php on line 22 [24-Nov-2025 10:11:16 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php on line 19 [24-Nov-2025 10:17:44 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php on line 17 [24-Nov-2025 10:18:39 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Link" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php on line 22 [25-Nov-2025 10:03:10 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Image.php on line 17 [25-Nov-2025 10:45:37 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Link" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Form.php on line 22 [25-Nov-2025 10:54:18 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\AbstractUriElement" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Link.php on line 19 Test/Constraint/CrawlerSelectorCount.php 0000644 00000002252 15112032670 0014427 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\DomCrawler\Crawler; final class CrawlerSelectorCount extends Constraint { public function __construct( private readonly int $count, private readonly string $selector, ) { } public function toString(): string { return sprintf('selector "%s" count is "%d"', $this->selector, $this->count); } /** * @param Crawler $crawler */ protected function matches($crawler): bool { return $this->count === \count($crawler->filter($this->selector)); } /** * @param Crawler $crawler */ protected function failureDescription($crawler): string { return sprintf('the Crawler selector "%s" was expected to be found %d time(s) but was found %d time(s)', $this->selector, $this->count, \count($crawler->filter($this->selector))); } } Test/Constraint/CrawlerSelectorTextSame.php 0000644 00000002443 15112032671 0015074 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\DomCrawler\Crawler; final class CrawlerSelectorTextSame extends Constraint { private string $selector; private string $expectedText; public function __construct(string $selector, string $expectedText) { $this->selector = $selector; $this->expectedText = $expectedText; } public function toString(): string { return sprintf('has a node matching selector "%s" with content "%s"', $this->selector, $this->expectedText); } /** * @param Crawler $crawler */ protected function matches($crawler): bool { $crawler = $crawler->filter($this->selector); if (!\count($crawler)) { return false; } return $this->expectedText === trim($crawler->text(null, true)); } /** * @param Crawler $crawler */ protected function failureDescription($crawler): string { return 'the Crawler '.$this->toString(); } } Test/Constraint/error_log 0000644 00000010223 15112032671 0011520 0 ustar 00 [25-Nov-2025 18:45:09 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextContains.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextContains.php on line 17 [25-Nov-2025 18:45:27 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php on line 17 [25-Nov-2025 18:46:10 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorExists.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorExists.php on line 17 [25-Nov-2025 19:52:52 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextSame.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextSame.php on line 17 [25-Nov-2025 20:20:35 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorCount.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorCount.php on line 17 [25-Nov-2025 20:20:37 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorCount.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorCount.php on line 17 [26-Nov-2025 17:38:24 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextSame.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextSame.php on line 17 [26-Nov-2025 19:18:54 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php on line 17 [26-Nov-2025 19:38:13 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorExists.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorExists.php on line 17 [26-Nov-2025 19:42:50 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextContains.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextContains.php on line 17 [26-Nov-2025 19:46:17 UTC] PHP Fatal error: Uncaught Error: Class "PHPUnit\Framework\Constraint\Constraint" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorCount.php:17 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Test/Constraint/CrawlerSelectorCount.php on line 17 Test/Constraint/CrawlerSelectorAttributeValueSame.php 0000644 00000002664 15112032671 0017115 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\DomCrawler\Crawler; final class CrawlerSelectorAttributeValueSame extends Constraint { private string $selector; private string $attribute; private string $expectedText; public function __construct(string $selector, string $attribute, string $expectedText) { $this->selector = $selector; $this->attribute = $attribute; $this->expectedText = $expectedText; } public function toString(): string { return sprintf('has a node matching selector "%s" with attribute "%s" of value "%s"', $this->selector, $this->attribute, $this->expectedText); } /** * @param Crawler $crawler */ protected function matches($crawler): bool { $crawler = $crawler->filter($this->selector); if (!\count($crawler)) { return false; } return $this->expectedText === trim($crawler->attr($this->attribute) ?? ''); } /** * @param Crawler $crawler */ protected function failureDescription($crawler): string { return 'the Crawler '.$this->toString(); } } Test/Constraint/CrawlerSelectorTextContains.php 0000644 00000003161 15112032671 0015763 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\DomCrawler\Crawler; final class CrawlerSelectorTextContains extends Constraint { private string $selector; private string $expectedText; private bool $hasNode = false; private string $nodeText; public function __construct(string $selector, string $expectedText) { $this->selector = $selector; $this->expectedText = $expectedText; } public function toString(): string { if ($this->hasNode) { return sprintf('the text "%s" of the node matching selector "%s" contains "%s"', $this->nodeText, $this->selector, $this->expectedText); } return sprintf('the Crawler has a node matching selector "%s"', $this->selector); } /** * @param Crawler $crawler */ protected function matches($crawler): bool { $crawler = $crawler->filter($this->selector); if (!\count($crawler)) { $this->hasNode = false; return false; } $this->hasNode = true; $this->nodeText = $crawler->text(null, true); return str_contains($this->nodeText, $this->expectedText); } /** * @param Crawler $crawler */ protected function failureDescription($crawler): string { return $this->toString(); } } Test/Constraint/CrawlerSelectorExists.php 0000644 00000002002 15112032671 0014610 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\DomCrawler\Crawler; final class CrawlerSelectorExists extends Constraint { private string $selector; public function __construct(string $selector) { $this->selector = $selector; } public function toString(): string { return sprintf('matches selector "%s"', $this->selector); } /** * @param Crawler $crawler */ protected function matches($crawler): bool { return 0 < \count($crawler->filter($this->selector)); } /** * @param Crawler $crawler */ protected function failureDescription($crawler): string { return 'the Crawler '.$this->toString(); } } FormFieldRegistry.php 0000644 00000011571 15112032671 0010660 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; use Symfony\Component\DomCrawler\Field\FormField; /** * This is an internal class that must not be used directly. * * @internal */ class FormFieldRegistry { private array $fields = []; private string $base = ''; /** * Adds a field to the registry. */ public function add(FormField $field): void { $segments = $this->getSegments($field->getName()); $target = &$this->fields; while ($segments) { if (!\is_array($target)) { $target = []; } $path = array_shift($segments); if ('' === $path) { $target = &$target[]; } else { $target = &$target[$path]; } } $target = $field; } /** * Removes a field based on the fully qualified name and its children from the registry. */ public function remove(string $name): void { $segments = $this->getSegments($name); $target = &$this->fields; while (\count($segments) > 1) { $path = array_shift($segments); if (!\is_array($target) || !\array_key_exists($path, $target)) { return; } $target = &$target[$path]; } unset($target[array_shift($segments)]); } /** * Returns the value of the field based on the fully qualified name and its children. * * @return FormField|FormField[]|FormField[][] * * @throws \InvalidArgumentException if the field does not exist */ public function &get(string $name): FormField|array { $segments = $this->getSegments($name); $target = &$this->fields; while ($segments) { $path = array_shift($segments); if (!\is_array($target) || !\array_key_exists($path, $target)) { throw new \InvalidArgumentException(sprintf('Unreachable field "%s".', $path)); } $target = &$target[$path]; } return $target; } /** * Tests whether the form has the given field based on the fully qualified name. */ public function has(string $name): bool { try { $this->get($name); return true; } catch (\InvalidArgumentException) { return false; } } /** * Set the value of a field based on the fully qualified name and its children. * * @throws \InvalidArgumentException if the field does not exist */ public function set(string $name, mixed $value): void { $target = &$this->get($name); if ((!\is_array($value) && $target instanceof Field\FormField) || $target instanceof Field\ChoiceFormField) { $target->setValue($value); } elseif (\is_array($value)) { $registry = new static(); $registry->base = $name; $registry->fields = $value; foreach ($registry->all() as $k => $v) { $this->set($k, $v); } } else { throw new \InvalidArgumentException(sprintf('Cannot set value on a compound field "%s".', $name)); } } /** * Returns the list of field with their value. * * @return FormField[] The list of fields as [string] Fully qualified name => (mixed) value) */ public function all(): array { return $this->walk($this->fields, $this->base); } /** * Transforms a PHP array in a list of fully qualified name / value. */ private function walk(array $array, ?string $base = '', array &$output = []): array { foreach ($array as $k => $v) { $path = empty($base) ? $k : sprintf('%s[%s]', $base, $k); if (\is_array($v)) { $this->walk($v, $path, $output); } else { $output[$path] = $v; } } return $output; } /** * Splits a field name into segments as a web browser would do. * * getSegments('base[foo][3][]') = ['base', 'foo, '3', '']; * * @return string[] */ private function getSegments(string $name): array { if (preg_match('/^(?P<base>[^[]+)(?P<extra>(\[.*)|$)/', $name, $m)) { $segments = [$m['base']]; while (!empty($m['extra'])) { $extra = $m['extra']; if (preg_match('/^\[(?P<segment>.*?)\](?P<extra>.*)$/', $extra, $m)) { $segments[] = $m['segment']; } else { $segments[] = $extra; } } return $segments; } return [$name]; } } Image.php 0000644 00000001622 15112032671 0006276 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; /** * Image represents an HTML image (an HTML img tag). */ class Image extends AbstractUriElement { public function __construct(\DOMElement $node, string $currentUri = null) { parent::__construct($node, $currentUri, 'GET'); } protected function getRawUri(): string { return $this->node->getAttribute('src'); } /** * @return void */ protected function setNode(\DOMElement $node) { if ('img' !== $node->nodeName) { throw new \LogicException(sprintf('Unable to visualize a "%s" tag.', $node->nodeName)); } $this->node = $node; } } CHANGELOG.md 0000644 00000007425 15112032672 0006364 0 ustar 00 CHANGELOG ========= 6.3 --- * Add `$useHtml5Parser` argument to `Crawler` * Add `CrawlerSelectorCount` test constraint * Add argument `$normalizeWhitespace` to `Crawler::innerText()` * Make `Crawler::innerText()` return the first non-empty text 6.0 --- * Remove `Crawler::parents()` method, use `ancestors()` instead 5.4 --- * Add `Crawler::innerText` method. 5.3 --- * The `parents()` method is deprecated. Use `ancestors()` instead. * Marked the `containsOption()`, `availableOptionValues()`, and `disableValidation()` methods of the `ChoiceFormField` class as internal 5.1.0 ----- * Added an internal cache layer on top of the CssSelectorConverter * Added `UriResolver` to resolve an URI according to a base URI 5.0.0 ----- * Added argument `$selector` to `Crawler::children()` * Added argument `$default` to `Crawler::text()` and `html()` 4.4.0 ----- * Added `Form::getName()` method. * Added `Crawler::matches()` method. * Added `Crawler::closest()` method. * Added `Crawler::outerHtml()` method. * Added an argument to the `Crawler::text()` method to opt-in normalizing whitespaces. 4.3.0 ----- * Added PHPUnit constraints: `CrawlerSelectorAttributeValueSame`, `CrawlerSelectorExists`, `CrawlerSelectorTextContains` and `CrawlerSelectorTextSame` * Added return of element name (`_name`) in `extract()` method. * Added ability to return a default value in `text()` and `html()` instead of throwing an exception when node is empty. * When available, the [html5-php library](https://github.com/Masterminds/html5-php) is used to parse HTML added to a Crawler for better support of HTML5 tags. 4.2.0 ----- * The `$currentUri` constructor argument of the `AbstractUriElement`, `Link` and `Image` classes is now optional. * The `Crawler::children()` method will have a new `$selector` argument in version 5.0, not defining it is deprecated. 3.1.0 ----- * All the URI parsing logic have been abstracted in the `AbstractUriElement` class. The `Link` class is now a child of `AbstractUriElement`. * Added an `Image` class to crawl images and parse their `src` attribute, and `selectImage`, `image`, `images` methods in the `Crawler` (the image version of the equivalent `link` methods). 2.5.0 ----- * [BC BREAK] The default value for checkbox and radio inputs without a value attribute have changed from '1' to 'on' to match the HTML specification. * [BC BREAK] The typehints on the `Link`, `Form` and `FormField` classes have been changed from `\DOMNode` to `DOMElement`. Using any other type of `DOMNode` was triggering fatal errors in previous versions. Code extending these classes will need to update the typehints when overwriting these methods. 2.4.0 ----- * `Crawler::addXmlContent()` removes the default document namespace again if it's an only namespace. * added support for automatic discovery and explicit registration of document namespaces for `Crawler::filterXPath()` and `Crawler::filter()` * improved content type guessing in `Crawler::addContent()` * [BC BREAK] `Crawler::addXmlContent()` no longer removes the default document namespace 2.3.0 ----- * added Crawler::html() * [BC BREAK] Crawler::each() and Crawler::reduce() now return Crawler instances instead of DomElement instances * added schema relative URL support to links * added support for HTML5 'form' attribute 2.2.0 ----- * added a way to set raw path to the file in FileFormField - necessary for simulating HTTP requests 2.1.0 ----- * added support for the HTTP PATCH method * refactored the Form class internals to support multi-dimensional fields (the public API is backward compatible) * added a way to get parsing errors for Crawler::addHtmlContent() and Crawler::addXmlContent() via libxml functions * added support for submitting a form without a submit button Field/InputFormField.php 0000644 00000002636 15112032672 0011175 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * InputFormField represents an input form field (an HTML input tag). * * For inputs with type of file, checkbox, or radio, there are other more * specialized classes (cf. FileFormField and ChoiceFormField). * * @author Fabien Potencier <fabien@symfony.com> */ class InputFormField extends FormField { /** * Initializes the form field. * * @return void * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('input' !== $this->node->nodeName && 'button' !== $this->node->nodeName) { throw new \LogicException(sprintf('An InputFormField can only be created from an input or button tag (%s given).', $this->node->nodeName)); } $type = strtolower($this->node->getAttribute('type')); if ('checkbox' === $type) { throw new \LogicException('Checkboxes should be instances of ChoiceFormField.'); } if ('file' === $type) { throw new \LogicException('File inputs should be instances of FileFormField.'); } $this->value = $this->node->getAttribute('value'); } } Field/FormField.php 0000644 00000005066 15112032672 0010155 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * FormField is the abstract class for all form fields. * * @author Fabien Potencier <fabien@symfony.com> */ abstract class FormField { /** * @var \DOMElement */ protected $node; /** * @var string */ protected $name; /** * @var string */ protected $value; /** * @var \DOMDocument */ protected $document; /** * @var \DOMXPath */ protected $xpath; /** * @var bool */ protected $disabled; /** * @param \DOMElement $node The node associated with this field */ public function __construct(\DOMElement $node) { $this->node = $node; $this->name = $node->getAttribute('name'); $this->xpath = new \DOMXPath($node->ownerDocument); $this->initialize(); } /** * Returns the label tag associated to the field or null if none. */ public function getLabel(): ?\DOMElement { $xpath = new \DOMXPath($this->node->ownerDocument); if ($this->node->hasAttribute('id')) { $labels = $xpath->query(sprintf('descendant::label[@for="%s"]', $this->node->getAttribute('id'))); if ($labels->length > 0) { return $labels->item(0); } } $labels = $xpath->query('ancestor::label[1]', $this->node); return $labels->length > 0 ? $labels->item(0) : null; } /** * Returns the name of the field. */ public function getName(): string { return $this->name; } /** * Gets the value of the field. */ public function getValue(): string|array|null { return $this->value; } /** * Sets the value of the field. * * @return void */ public function setValue(?string $value) { $this->value = $value ?? ''; } /** * Returns true if the field should be included in the submitted values. */ public function hasValue(): bool { return true; } /** * Check if the current field is disabled. */ public function isDisabled(): bool { return $this->node->hasAttribute('disabled'); } /** * Initializes the form field. * * @return void */ abstract protected function initialize(); } Field/ChoiceFormField.php 0000644 00000021473 15112032672 0011270 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * ChoiceFormField represents a choice form field. * * It is constructed from an HTML select tag, or an HTML checkbox, or radio inputs. * * @author Fabien Potencier <fabien@symfony.com> */ class ChoiceFormField extends FormField { private string $type; private bool $multiple; private array $options; private bool $validationDisabled = false; /** * Returns true if the field should be included in the submitted values. * * @return bool true if the field should be included in the submitted values, false otherwise */ public function hasValue(): bool { // don't send a value for unchecked checkboxes if (\in_array($this->type, ['checkbox', 'radio']) && null === $this->value) { return false; } return true; } /** * Check if the current selected option is disabled. */ public function isDisabled(): bool { if (parent::isDisabled() && 'select' === $this->type) { return true; } foreach ($this->options as $option) { if ($option['value'] == $this->value && $option['disabled']) { return true; } } return false; } /** * Sets the value of the field. * * @return void */ public function select(string|array|bool $value) { $this->setValue($value); } /** * Ticks a checkbox. * * @return void * * @throws \LogicException When the type provided is not correct */ public function tick() { if ('checkbox' !== $this->type) { throw new \LogicException(sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->name, $this->type)); } $this->setValue(true); } /** * Unticks a checkbox. * * @return void * * @throws \LogicException When the type provided is not correct */ public function untick() { if ('checkbox' !== $this->type) { throw new \LogicException(sprintf('You cannot untick "%s" as it is not a checkbox (%s).', $this->name, $this->type)); } $this->setValue(false); } /** * Sets the value of the field. * * @return void * * @throws \InvalidArgumentException When value type provided is not correct */ public function setValue(string|array|bool|null $value) { if ('checkbox' === $this->type && false === $value) { // uncheck $this->value = null; } elseif ('checkbox' === $this->type && true === $value) { // check $this->value = $this->options[0]['value']; } else { if (\is_array($value)) { if (!$this->multiple) { throw new \InvalidArgumentException(sprintf('The value for "%s" cannot be an array.', $this->name)); } foreach ($value as $v) { if (!$this->containsOption($v, $this->options)) { throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: "%s").', $this->name, $v, implode('", "', $this->availableOptionValues()))); } } } elseif (!$this->containsOption($value, $this->options)) { throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: "%s").', $this->name, $value, implode('", "', $this->availableOptionValues()))); } if ($this->multiple) { $value = (array) $value; } if (\is_array($value)) { $this->value = $value; } else { parent::setValue($value); } } } /** * Adds a choice to the current ones. * * @throws \LogicException When choice provided is not multiple nor radio * * @internal */ public function addChoice(\DOMElement $node): void { if (!$this->multiple && 'radio' !== $this->type) { throw new \LogicException(sprintf('Unable to add a choice for "%s" as it is not multiple or is not a radio button.', $this->name)); } $option = $this->buildOptionValue($node); $this->options[] = $option; if ($node->hasAttribute('checked')) { $this->value = $option['value']; } } /** * Returns the type of the choice field (radio, select, or checkbox). */ public function getType(): string { return $this->type; } /** * Returns true if the field accepts multiple values. */ public function isMultiple(): bool { return $this->multiple; } /** * Initializes the form field. * * @return void * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('input' !== $this->node->nodeName && 'select' !== $this->node->nodeName) { throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input or select tag (%s given).', $this->node->nodeName)); } if ('input' === $this->node->nodeName && 'checkbox' !== strtolower($this->node->getAttribute('type')) && 'radio' !== strtolower($this->node->getAttribute('type'))) { throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input tag with a type of checkbox or radio (given type is "%s").', $this->node->getAttribute('type'))); } $this->value = null; $this->options = []; $this->multiple = false; if ('input' == $this->node->nodeName) { $this->type = strtolower($this->node->getAttribute('type')); $optionValue = $this->buildOptionValue($this->node); $this->options[] = $optionValue; if ($this->node->hasAttribute('checked')) { $this->value = $optionValue['value']; } } else { $this->type = 'select'; if ($this->node->hasAttribute('multiple')) { $this->multiple = true; $this->value = []; $this->name = str_replace('[]', '', $this->name); } $found = false; foreach ($this->xpath->query('descendant::option', $this->node) as $option) { $optionValue = $this->buildOptionValue($option); $this->options[] = $optionValue; if ($option->hasAttribute('selected')) { $found = true; if ($this->multiple) { $this->value[] = $optionValue['value']; } else { $this->value = $optionValue['value']; } } } // if no option is selected and if it is a simple select box, take the first option as the value if (!$found && !$this->multiple && $this->options) { $this->value = $this->options[0]['value']; } } } /** * Returns option value with associated disabled flag. */ private function buildOptionValue(\DOMElement $node): array { $option = []; $defaultDefaultValue = 'select' === $this->node->nodeName ? '' : 'on'; $defaultValue = (isset($node->nodeValue) && !empty($node->nodeValue)) ? $node->nodeValue : $defaultDefaultValue; $option['value'] = $node->hasAttribute('value') ? $node->getAttribute('value') : $defaultValue; $option['disabled'] = $node->hasAttribute('disabled'); return $option; } /** * Checks whether given value is in the existing options. * * @internal */ public function containsOption(string $optionValue, array $options): bool { if ($this->validationDisabled) { return true; } foreach ($options as $option) { if ($option['value'] == $optionValue) { return true; } } return false; } /** * Returns list of available field options. * * @internal */ public function availableOptionValues(): array { $values = []; foreach ($this->options as $option) { $values[] = $option['value']; } return $values; } /** * Disables the internal validation of the field. * * @internal * * @return $this */ public function disableValidation(): static { $this->validationDisabled = true; return $this; } } Field/error_log 0000644 00000010172 15112032672 0007504 0 ustar 00 [19-Nov-2025 12:04:57 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/ChoiceFormField.php:21 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/ChoiceFormField.php on line 21 [19-Nov-2025 13:22:28 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/InputFormField.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/InputFormField.php on line 22 [20-Nov-2025 01:15:25 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/FileFormField.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/FileFormField.php on line 19 [20-Nov-2025 06:44:40 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/TextareaFormField.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/TextareaFormField.php on line 19 [25-Nov-2025 03:00:33 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/FileFormField.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/FileFormField.php on line 19 [25-Nov-2025 03:03:12 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/ChoiceFormField.php:21 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/ChoiceFormField.php on line 21 [25-Nov-2025 05:27:14 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/TextareaFormField.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/TextareaFormField.php on line 19 [25-Nov-2025 05:29:53 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/InputFormField.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/InputFormField.php on line 22 [25-Nov-2025 23:06:10 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/InputFormField.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/InputFormField.php on line 22 [25-Nov-2025 23:10:50 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/TextareaFormField.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/TextareaFormField.php on line 19 [26-Nov-2025 01:21:22 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/FileFormField.php:19 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/FileFormField.php on line 19 [26-Nov-2025 01:54:20 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\DomCrawler\Field\FormField" not found in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/ChoiceFormField.php:21 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/dom-crawler/Field/ChoiceFormField.php on line 21 Field/FileFormField.php 0000644 00000006544 15112032672 0010757 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * FileFormField represents a file form field (an HTML file input tag). * * @author Fabien Potencier <fabien@symfony.com> */ class FileFormField extends FormField { /** * Sets the PHP error code associated with the field. * * @param int $error The error code (one of UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, or UPLOAD_ERR_EXTENSION) * * @return void * * @throws \InvalidArgumentException When error code doesn't exist */ public function setErrorCode(int $error) { $codes = [\UPLOAD_ERR_INI_SIZE, \UPLOAD_ERR_FORM_SIZE, \UPLOAD_ERR_PARTIAL, \UPLOAD_ERR_NO_FILE, \UPLOAD_ERR_NO_TMP_DIR, \UPLOAD_ERR_CANT_WRITE, \UPLOAD_ERR_EXTENSION]; if (!\in_array($error, $codes)) { throw new \InvalidArgumentException(sprintf('The error code "%s" is not valid.', $error)); } $this->value = ['name' => '', 'type' => '', 'tmp_name' => '', 'error' => $error, 'size' => 0]; } /** * Sets the value of the field. * * @return void */ public function upload(?string $value) { $this->setValue($value); } /** * Sets the value of the field. * * @return void */ public function setValue(?string $value) { if (null !== $value && is_readable($value)) { $error = \UPLOAD_ERR_OK; $size = filesize($value); $info = pathinfo($value); $name = $info['basename']; // copy to a tmp location $tmp = sys_get_temp_dir().'/'.strtr(substr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), 0, 7), '/', '_'); if (\array_key_exists('extension', $info)) { $tmp .= '.'.$info['extension']; } if (is_file($tmp)) { unlink($tmp); } copy($value, $tmp); $value = $tmp; } else { $error = \UPLOAD_ERR_NO_FILE; $size = 0; $name = ''; $value = ''; } $this->value = ['name' => $name, 'type' => '', 'tmp_name' => $value, 'error' => $error, 'size' => $size]; } /** * Sets path to the file as string for simulating HTTP request. * * @return void */ public function setFilePath(string $path) { parent::setValue($path); } /** * Initializes the form field. * * @return void * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('input' !== $this->node->nodeName) { throw new \LogicException(sprintf('A FileFormField can only be created from an input tag (%s given).', $this->node->nodeName)); } if ('file' !== strtolower($this->node->getAttribute('type'))) { throw new \LogicException(sprintf('A FileFormField can only be created from an input tag with a type of file (given type is "%s").', $this->node->getAttribute('type'))); } $this->setValue(null); } } Field/TextareaFormField.php 0000644 00000001756 15112032672 0011655 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * TextareaFormField represents a textarea form field (an HTML textarea tag). * * @author Fabien Potencier <fabien@symfony.com> */ class TextareaFormField extends FormField { /** * Initializes the form field. * * @return void * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('textarea' !== $this->node->nodeName) { throw new \LogicException(sprintf('A TextareaFormField can only be created from a textarea tag (%s given).', $this->node->nodeName)); } $this->value = ''; foreach ($this->node->childNodes as $node) { $this->value .= $node->wholeText; } } } Crawler.php 0000644 00000113614 15112032673 0006662 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; use Masterminds\HTML5; use Symfony\Component\CssSelector\CssSelectorConverter; /** * Crawler eases navigation of a list of \DOMNode objects. * * @author Fabien Potencier <fabien@symfony.com> * * @implements \IteratorAggregate<int, \DOMNode> */ class Crawler implements \Countable, \IteratorAggregate { /** * @var string|null */ protected $uri; /** * The default namespace prefix to be used with XPath and CSS expressions. */ private string $defaultNamespacePrefix = 'default'; /** * A map of manually registered namespaces. * * @var array<string, string> */ private array $namespaces = []; /** * A map of cached namespaces. */ private \ArrayObject $cachedNamespaces; private ?string $baseHref; private ?\DOMDocument $document = null; /** * @var list<\DOMNode> */ private array $nodes = []; /** * Whether the Crawler contains HTML or XML content (used when converting CSS to XPath). */ private bool $isHtml = true; private ?HTML5 $html5Parser = null; /** * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $node A Node to use as the base for the crawling */ public function __construct(\DOMNodeList|\DOMNode|array|string $node = null, string $uri = null, string $baseHref = null, bool $useHtml5Parser = true) { $this->uri = $uri; $this->baseHref = $baseHref ?: $uri; $this->html5Parser = $useHtml5Parser ? new HTML5(['disable_html_ns' => true]) : null; $this->cachedNamespaces = new \ArrayObject(); $this->add($node); } /** * Returns the current URI. */ public function getUri(): ?string { return $this->uri; } /** * Returns base href. */ public function getBaseHref(): ?string { return $this->baseHref; } /** * Removes all the nodes. * * @return void */ public function clear() { $this->nodes = []; $this->document = null; $this->cachedNamespaces = new \ArrayObject(); } /** * Adds a node to the current list of nodes. * * This method uses the appropriate specialized add*() method based * on the type of the argument. * * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $node A node * * @return void * * @throws \InvalidArgumentException when node is not the expected type */ public function add(\DOMNodeList|\DOMNode|array|string|null $node) { if ($node instanceof \DOMNodeList) { $this->addNodeList($node); } elseif ($node instanceof \DOMNode) { $this->addNode($node); } elseif (\is_array($node)) { $this->addNodes($node); } elseif (\is_string($node)) { $this->addContent($node); } elseif (null !== $node) { throw new \InvalidArgumentException(sprintf('Expecting a DOMNodeList or DOMNode instance, an array, a string, or null, but got "%s".', get_debug_type($node))); } } /** * Adds HTML/XML content. * * If the charset is not set via the content type, it is assumed to be UTF-8, * or ISO-8859-1 as a fallback, which is the default charset defined by the * HTTP 1.1 specification. * * @return void */ public function addContent(string $content, string $type = null) { if (empty($type)) { $type = str_starts_with($content, '<?xml') ? 'application/xml' : 'text/html'; } // DOM only for HTML/XML content if (!preg_match('/(x|ht)ml/i', $type, $xmlMatches)) { return; } $charset = preg_match('//u', $content) ? 'UTF-8' : 'ISO-8859-1'; // http://www.w3.org/TR/encoding/#encodings // http://www.w3.org/TR/REC-xml/#NT-EncName $content = preg_replace_callback('/(charset *= *["\']?)([a-zA-Z\-0-9_:.]+)/i', function ($m) use (&$charset) { if ('charset=' === $this->convertToHtmlEntities('charset=', $m[2])) { $charset = $m[2]; } return $m[1].$charset; }, $content, 1); if ('x' === $xmlMatches[1]) { $this->addXmlContent($content, $charset); } else { $this->addHtmlContent($content, $charset); } } /** * Adds an HTML content to the list of nodes. * * The libxml errors are disabled when the content is parsed. * * If you want to get parsing errors, be sure to enable * internal errors via libxml_use_internal_errors(true) * and then, get the errors via libxml_get_errors(). Be * sure to clear errors with libxml_clear_errors() afterward. * * @return void */ public function addHtmlContent(string $content, string $charset = 'UTF-8') { $dom = $this->parseHtmlString($content, $charset); $this->addDocument($dom); $base = $this->filterRelativeXPath('descendant-or-self::base')->extract(['href']); $baseHref = current($base); if (\count($base) && !empty($baseHref)) { if ($this->baseHref) { $linkNode = $dom->createElement('a'); $linkNode->setAttribute('href', $baseHref); $link = new Link($linkNode, $this->baseHref); $this->baseHref = $link->getUri(); } else { $this->baseHref = $baseHref; } } } /** * Adds an XML content to the list of nodes. * * The libxml errors are disabled when the content is parsed. * * If you want to get parsing errors, be sure to enable * internal errors via libxml_use_internal_errors(true) * and then, get the errors via libxml_get_errors(). Be * sure to clear errors with libxml_clear_errors() afterward. * * @param int $options Bitwise OR of the libxml option constants * LIBXML_PARSEHUGE is dangerous, see * http://symfony.com/blog/security-release-symfony-2-0-17-released * * @return void */ public function addXmlContent(string $content, string $charset = 'UTF-8', int $options = \LIBXML_NONET) { // remove the default namespace if it's the only namespace to make XPath expressions simpler if (!str_contains($content, 'xmlns:')) { $content = str_replace('xmlns', 'ns', $content); } $internalErrors = libxml_use_internal_errors(true); $dom = new \DOMDocument('1.0', $charset); $dom->validateOnParse = true; if ('' !== trim($content)) { @$dom->loadXML($content, $options); } libxml_use_internal_errors($internalErrors); $this->addDocument($dom); $this->isHtml = false; } /** * Adds a \DOMDocument to the list of nodes. * * @param \DOMDocument $dom A \DOMDocument instance * * @return void */ public function addDocument(\DOMDocument $dom) { if ($dom->documentElement) { $this->addNode($dom->documentElement); } } /** * Adds a \DOMNodeList to the list of nodes. * * @param \DOMNodeList $nodes A \DOMNodeList instance * * @return void */ public function addNodeList(\DOMNodeList $nodes) { foreach ($nodes as $node) { if ($node instanceof \DOMNode) { $this->addNode($node); } } } /** * Adds an array of \DOMNode instances to the list of nodes. * * @param \DOMNode[] $nodes An array of \DOMNode instances * * @return void */ public function addNodes(array $nodes) { foreach ($nodes as $node) { $this->add($node); } } /** * Adds a \DOMNode instance to the list of nodes. * * @param \DOMNode $node A \DOMNode instance * * @return void */ public function addNode(\DOMNode $node) { if ($node instanceof \DOMDocument) { $node = $node->documentElement; } if (null !== $this->document && $this->document !== $node->ownerDocument) { throw new \InvalidArgumentException('Attaching DOM nodes from multiple documents in the same crawler is forbidden.'); } $this->document ??= $node->ownerDocument; // Don't add duplicate nodes in the Crawler if (\in_array($node, $this->nodes, true)) { return; } $this->nodes[] = $node; } /** * Returns a node given its position in the node list. */ public function eq(int $position): static { if (isset($this->nodes[$position])) { return $this->createSubCrawler($this->nodes[$position]); } return $this->createSubCrawler(null); } /** * Calls an anonymous function on each node of the list. * * The anonymous function receives the position and the node wrapped * in a Crawler instance as arguments. * * Example: * * $crawler->filter('h1')->each(function ($node, $i) { * return $node->text(); * }); * * @param \Closure $closure An anonymous function * * @return array An array of values returned by the anonymous function */ public function each(\Closure $closure): array { $data = []; foreach ($this->nodes as $i => $node) { $data[] = $closure($this->createSubCrawler($node), $i); } return $data; } /** * Slices the list of nodes by $offset and $length. */ public function slice(int $offset = 0, int $length = null): static { return $this->createSubCrawler(\array_slice($this->nodes, $offset, $length)); } /** * Reduces the list of nodes by calling an anonymous function. * * To remove a node from the list, the anonymous function must return false. * * @param \Closure $closure An anonymous function */ public function reduce(\Closure $closure): static { $nodes = []; foreach ($this->nodes as $i => $node) { if (false !== $closure($this->createSubCrawler($node), $i)) { $nodes[] = $node; } } return $this->createSubCrawler($nodes); } /** * Returns the first node of the current selection. */ public function first(): static { return $this->eq(0); } /** * Returns the last node of the current selection. */ public function last(): static { return $this->eq(\count($this->nodes) - 1); } /** * Returns the siblings nodes of the current selection. * * @throws \InvalidArgumentException When current node is empty */ public function siblings(): static { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild)); } public function matches(string $selector): bool { if (!$this->nodes) { return false; } $converter = $this->createCssSelectorConverter(); $xpath = $converter->toXPath($selector, 'self::'); return 0 !== $this->filterRelativeXPath($xpath)->count(); } /** * Return first parents (heading toward the document root) of the Element that matches the provided selector. * * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill * * @throws \InvalidArgumentException When current node is empty */ public function closest(string $selector): ?self { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } $domNode = $this->getNode(0); while (\XML_ELEMENT_NODE === $domNode->nodeType) { $node = $this->createSubCrawler($domNode); if ($node->matches($selector)) { return $node; } $domNode = $node->getNode(0)->parentNode; } return null; } /** * Returns the next siblings nodes of the current selection. * * @throws \InvalidArgumentException When current node is empty */ public function nextAll(): static { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } return $this->createSubCrawler($this->sibling($this->getNode(0))); } /** * Returns the previous sibling nodes of the current selection. * * @throws \InvalidArgumentException */ public function previousAll(): static { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } return $this->createSubCrawler($this->sibling($this->getNode(0), 'previousSibling')); } /** * Returns the ancestors of the current selection. * * @throws \InvalidArgumentException When the current node is empty */ public function ancestors(): static { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); $nodes = []; while ($node = $node->parentNode) { if (\XML_ELEMENT_NODE === $node->nodeType) { $nodes[] = $node; } } return $this->createSubCrawler($nodes); } /** * Returns the children nodes of the current selection. * * @throws \InvalidArgumentException When current node is empty * @throws \RuntimeException If the CssSelector Component is not available and $selector is provided */ public function children(string $selector = null): static { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } if (null !== $selector) { $converter = $this->createCssSelectorConverter(); $xpath = $converter->toXPath($selector, 'child::'); return $this->filterRelativeXPath($xpath); } $node = $this->getNode(0)->firstChild; return $this->createSubCrawler($node ? $this->sibling($node) : []); } /** * Returns the attribute value of the first node of the list. * * @throws \InvalidArgumentException When current node is empty */ public function attr(string $attribute): ?string { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); return $node->hasAttribute($attribute) ? $node->getAttribute($attribute) : null; } /** * Returns the node name of the first node of the list. * * @throws \InvalidArgumentException When current node is empty */ public function nodeName(): string { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } return $this->getNode(0)->nodeName; } /** * Returns the text of the first node of the list. * * Pass true as the second argument to normalize whitespaces. * * @param string|null $default When not null: the value to return when the current node is empty * @param bool $normalizeWhitespace Whether whitespaces should be trimmed and normalized to single spaces * * @throws \InvalidArgumentException When current node is empty */ public function text(string $default = null, bool $normalizeWhitespace = true): string { if (!$this->nodes) { if (null !== $default) { return $default; } throw new \InvalidArgumentException('The current node list is empty.'); } $text = $this->getNode(0)->nodeValue; if ($normalizeWhitespace) { return $this->normalizeWhitespace($text); } return $text; } /** * Returns only the inner text that is the direct descendent of the current node, excluding any child nodes. * * @param bool $normalizeWhitespace Whether whitespaces should be trimmed and normalized to single spaces */ public function innerText(/* bool $normalizeWhitespace = true */): string { $normalizeWhitespace = 1 <= \func_num_args() ? func_get_arg(0) : true; foreach ($this->getNode(0)->childNodes as $childNode) { if (\XML_TEXT_NODE !== $childNode->nodeType && \XML_CDATA_SECTION_NODE !== $childNode->nodeType) { continue; } if (!$normalizeWhitespace) { return $childNode->nodeValue; } if ('' !== trim($childNode->nodeValue)) { return $this->normalizeWhitespace($childNode->nodeValue); } } return ''; } /** * Returns the first node of the list as HTML. * * @param string|null $default When not null: the value to return when the current node is empty * * @throws \InvalidArgumentException When current node is empty */ public function html(string $default = null): string { if (!$this->nodes) { if (null !== $default) { return $default; } throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); $owner = $node->ownerDocument; if ($this->html5Parser && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) { $owner = $this->html5Parser; } $html = ''; foreach ($node->childNodes as $child) { $html .= $owner->saveHTML($child); } return $html; } public function outerHtml(): string { if (!\count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); $owner = $node->ownerDocument; if ($this->html5Parser && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) { $owner = $this->html5Parser; } return $owner->saveHTML($node); } /** * Evaluates an XPath expression. * * Since an XPath expression might evaluate to either a simple type or a \DOMNodeList, * this method will return either an array of simple types or a new Crawler instance. */ public function evaluate(string $xpath): array|Crawler { if (null === $this->document) { throw new \LogicException('Cannot evaluate the expression on an uninitialized crawler.'); } $data = []; $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath)); foreach ($this->nodes as $node) { $data[] = $domxpath->evaluate($xpath, $node); } if (isset($data[0]) && $data[0] instanceof \DOMNodeList) { return $this->createSubCrawler($data); } return $data; } /** * Extracts information from the list of nodes. * * You can extract attributes or/and the node value (_text). * * Example: * * $crawler->filter('h1 a')->extract(['_text', 'href']); */ public function extract(array $attributes): array { $count = \count($attributes); $data = []; foreach ($this->nodes as $node) { $elements = []; foreach ($attributes as $attribute) { if ('_text' === $attribute) { $elements[] = $node->nodeValue; } elseif ('_name' === $attribute) { $elements[] = $node->nodeName; } else { $elements[] = $node->getAttribute($attribute); } } $data[] = 1 === $count ? $elements[0] : $elements; } return $data; } /** * Filters the list of nodes with an XPath expression. * * The XPath expression is evaluated in the context of the crawler, which * is considered as a fake parent of the elements inside it. * This means that a child selector "div" or "./div" will match only * the div elements of the current crawler, not their children. */ public function filterXPath(string $xpath): static { $xpath = $this->relativize($xpath); // If we dropped all expressions in the XPath while preparing it, there would be no match if ('' === $xpath) { return $this->createSubCrawler(null); } return $this->filterRelativeXPath($xpath); } /** * Filters the list of nodes with a CSS selector. * * This method only works if you have installed the CssSelector Symfony Component. * * @throws \LogicException if the CssSelector Component is not available */ public function filter(string $selector): static { $converter = $this->createCssSelectorConverter(); // The CssSelector already prefixes the selector with descendant-or-self:: return $this->filterRelativeXPath($converter->toXPath($selector)); } /** * Selects links by name or alt value for clickable images. */ public function selectLink(string $value): static { return $this->filterRelativeXPath( sprintf('descendant-or-self::a[contains(concat(\' \', normalize-space(string(.)), \' \'), %1$s) or ./img[contains(concat(\' \', normalize-space(string(@alt)), \' \'), %1$s)]]', static::xpathLiteral(' '.$value.' ')) ); } /** * Selects images by alt value. */ public function selectImage(string $value): static { $xpath = sprintf('descendant-or-self::img[contains(normalize-space(string(@alt)), %s)]', static::xpathLiteral($value)); return $this->filterRelativeXPath($xpath); } /** * Selects a button by name or alt value for images. */ public function selectButton(string $value): static { return $this->filterRelativeXPath( sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value)) ); } /** * Returns a Link object for the first node in the list. * * @throws \InvalidArgumentException If the current node list is empty or the selected node is not instance of DOMElement */ public function link(string $method = 'get'): Link { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); if (!$node instanceof \DOMElement) { throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node))); } return new Link($node, $this->baseHref, $method); } /** * Returns an array of Link objects for the nodes in the list. * * @return Link[] * * @throws \InvalidArgumentException If the current node list contains non-DOMElement instances */ public function links(): array { $links = []; foreach ($this->nodes as $node) { if (!$node instanceof \DOMElement) { throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_debug_type($node))); } $links[] = new Link($node, $this->baseHref, 'get'); } return $links; } /** * Returns an Image object for the first node in the list. * * @throws \InvalidArgumentException If the current node list is empty */ public function image(): Image { if (!\count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); if (!$node instanceof \DOMElement) { throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node))); } return new Image($node, $this->baseHref); } /** * Returns an array of Image objects for the nodes in the list. * * @return Image[] */ public function images(): array { $images = []; foreach ($this as $node) { if (!$node instanceof \DOMElement) { throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_debug_type($node))); } $images[] = new Image($node, $this->baseHref); } return $images; } /** * Returns a Form object for the first node in the list. * * @throws \InvalidArgumentException If the current node list is empty or the selected node is not instance of DOMElement */ public function form(array $values = null, string $method = null): Form { if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); if (!$node instanceof \DOMElement) { throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node))); } $form = new Form($node, $this->uri, $method, $this->baseHref); if (null !== $values) { $form->setValues($values); } return $form; } /** * Overloads a default namespace prefix to be used with XPath and CSS expressions. * * @return void */ public function setDefaultNamespacePrefix(string $prefix) { $this->defaultNamespacePrefix = $prefix; } /** * @return void */ public function registerNamespace(string $prefix, string $namespace) { $this->namespaces[$prefix] = $namespace; } /** * Converts string for XPath expressions. * * Escaped characters are: quotes (") and apostrophe ('). * * Examples: * * echo Crawler::xpathLiteral('foo " bar'); * //prints 'foo " bar' * * echo Crawler::xpathLiteral("foo ' bar"); * //prints "foo ' bar" * * echo Crawler::xpathLiteral('a\'b"c'); * //prints concat('a', "'", 'b"c') */ public static function xpathLiteral(string $s): string { if (!str_contains($s, "'")) { return sprintf("'%s'", $s); } if (!str_contains($s, '"')) { return sprintf('"%s"', $s); } $string = $s; $parts = []; while (true) { if (false !== $pos = strpos($string, "'")) { $parts[] = sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { $parts[] = "'$string'"; break; } } return sprintf('concat(%s)', implode(', ', $parts)); } /** * Filters the list of nodes with an XPath expression. * * The XPath expression should already be processed to apply it in the context of each node. */ private function filterRelativeXPath(string $xpath): static { $crawler = $this->createSubCrawler(null); if (null === $this->document) { return $crawler; } $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath)); foreach ($this->nodes as $node) { $crawler->add($domxpath->query($xpath, $node)); } return $crawler; } /** * Make the XPath relative to the current context. * * The returned XPath will match elements matching the XPath inside the current crawler * when running in the context of a node of the crawler. */ private function relativize(string $xpath): string { $expressions = []; // An expression which will never match to replace expressions which cannot match in the crawler // We cannot drop $nonMatchingExpression = 'a[name() = "b"]'; $xpathLen = \strlen($xpath); $openedBrackets = 0; $startPosition = strspn($xpath, " \t\n\r\0\x0B"); for ($i = $startPosition; $i <= $xpathLen; ++$i) { $i += strcspn($xpath, '"\'[]|', $i); if ($i < $xpathLen) { switch ($xpath[$i]) { case '"': case "'": if (false === $i = strpos($xpath, $xpath[$i], $i + 1)) { return $xpath; // The XPath expression is invalid } continue 2; case '[': ++$openedBrackets; continue 2; case ']': --$openedBrackets; continue 2; } } if ($openedBrackets) { continue; } if ($startPosition < $xpathLen && '(' === $xpath[$startPosition]) { // If the union is inside some braces, we need to preserve the opening braces and apply // the change only inside it. $j = 1 + strspn($xpath, "( \t\n\r\0\x0B", $startPosition + 1); $parenthesis = substr($xpath, $startPosition, $j); $startPosition += $j; } else { $parenthesis = ''; } $expression = rtrim(substr($xpath, $startPosition, $i - $startPosition)); if (str_starts_with($expression, 'self::*/')) { $expression = './'.substr($expression, 8); } // add prefix before absolute element selector if ('' === $expression) { $expression = $nonMatchingExpression; } elseif (str_starts_with($expression, '//')) { $expression = 'descendant-or-self::'.substr($expression, 2); } elseif (str_starts_with($expression, './/')) { $expression = 'descendant-or-self::'.substr($expression, 3); } elseif (str_starts_with($expression, './')) { $expression = 'self::'.substr($expression, 2); } elseif (str_starts_with($expression, 'child::')) { $expression = 'self::'.substr($expression, 7); } elseif ('/' === $expression[0] || '.' === $expression[0] || str_starts_with($expression, 'self::')) { $expression = $nonMatchingExpression; } elseif (str_starts_with($expression, 'descendant::')) { $expression = 'descendant-or-self::'.substr($expression, 12); } elseif (preg_match('/^(ancestor|ancestor-or-self|attribute|following|following-sibling|namespace|parent|preceding|preceding-sibling)::/', $expression)) { // the fake root has no parent, preceding or following nodes and also no attributes (even no namespace attributes) $expression = $nonMatchingExpression; } elseif (!str_starts_with($expression, 'descendant-or-self::')) { $expression = 'self::'.$expression; } $expressions[] = $parenthesis.$expression; if ($i === $xpathLen) { return implode(' | ', $expressions); } $i += strspn($xpath, " \t\n\r\0\x0B", $i + 1); $startPosition = $i + 1; } return $xpath; // The XPath expression is invalid } public function getNode(int $position): ?\DOMNode { return $this->nodes[$position] ?? null; } public function count(): int { return \count($this->nodes); } /** * @return \ArrayIterator<int, \DOMNode> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->nodes); } protected function sibling(\DOMNode $node, string $siblingDir = 'nextSibling'): array { $nodes = []; $currentNode = $this->getNode(0); do { if ($node !== $currentNode && \XML_ELEMENT_NODE === $node->nodeType) { $nodes[] = $node; } } while ($node = $node->$siblingDir); return $nodes; } private function parseHtml5(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument { return $this->html5Parser->parse($this->convertToHtmlEntities($htmlContent, $charset)); } private function parseXhtml(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument { $htmlContent = $this->convertToHtmlEntities($htmlContent, $charset); $internalErrors = libxml_use_internal_errors(true); $dom = new \DOMDocument('1.0', $charset); $dom->validateOnParse = true; if ('' !== trim($htmlContent)) { @$dom->loadHTML($htmlContent); } libxml_use_internal_errors($internalErrors); return $dom; } /** * Converts charset to HTML-entities to ensure valid parsing. */ private function convertToHtmlEntities(string $htmlContent, string $charset = 'UTF-8'): string { set_error_handler(function () { throw new \Exception(); }); try { return mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], $charset); } catch (\Exception|\ValueError) { try { $htmlContent = iconv($charset, 'UTF-8', $htmlContent); $htmlContent = mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); } catch (\Exception|\ValueError) { } return $htmlContent; } finally { restore_error_handler(); } } /** * @throws \InvalidArgumentException */ private function createDOMXPath(\DOMDocument $document, array $prefixes = []): \DOMXPath { $domxpath = new \DOMXPath($document); foreach ($prefixes as $prefix) { $namespace = $this->discoverNamespace($domxpath, $prefix); if (null !== $namespace) { $domxpath->registerNamespace($prefix, $namespace); } } return $domxpath; } /** * @throws \InvalidArgumentException */ private function discoverNamespace(\DOMXPath $domxpath, string $prefix): ?string { if (\array_key_exists($prefix, $this->namespaces)) { return $this->namespaces[$prefix]; } if ($this->cachedNamespaces->offsetExists($prefix)) { return $this->cachedNamespaces[$prefix]; } // ask for one namespace, otherwise we'd get a collection with an item for each node $namespaces = $domxpath->query(sprintf('(//namespace::*[name()="%s"])[last()]', $this->defaultNamespacePrefix === $prefix ? '' : $prefix)); return $this->cachedNamespaces[$prefix] = ($node = $namespaces->item(0)) ? $node->nodeValue : null; } private function findNamespacePrefixes(string $xpath): array { if (preg_match_all('/(?P<prefix>[a-z_][a-z_0-9\-\.]*+):[^"\/:]/i', $xpath, $matches)) { return array_unique($matches['prefix']); } return []; } /** * Creates a crawler for some subnodes. * * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $nodes */ private function createSubCrawler(\DOMNodeList|\DOMNode|array|string|null $nodes): static { $crawler = new static($nodes, $this->uri, $this->baseHref); $crawler->isHtml = $this->isHtml; $crawler->document = $this->document; $crawler->namespaces = $this->namespaces; $crawler->cachedNamespaces = $this->cachedNamespaces; $crawler->html5Parser = $this->html5Parser; return $crawler; } /** * @throws \LogicException If the CssSelector Component is not available */ private function createCssSelectorConverter(): CssSelectorConverter { if (!class_exists(CssSelectorConverter::class)) { throw new \LogicException('To filter with a CSS selector, install the CssSelector component ("composer require symfony/css-selector"). Or use filterXpath instead.'); } return new CssSelectorConverter($this->isHtml); } /** * Parse string into DOMDocument object using HTML5 parser if the content is HTML5 and the library is available. * Use libxml parser otherwise. */ private function parseHtmlString(string $content, string $charset): \DOMDocument { if ($this->canParseHtml5String($content)) { return $this->parseHtml5($content, $charset); } return $this->parseXhtml($content, $charset); } private function canParseHtml5String(string $content): bool { if (!$this->html5Parser) { return false; } if (false === ($pos = stripos($content, '<!doctype html>'))) { return false; } $header = substr($content, 0, $pos); return '' === $header || $this->isValidHtml5Heading($header); } private function isValidHtml5Heading(string $heading): bool { return 1 === preg_match('/^\x{FEFF}?\s*(<!--[^>]*?-->\s*)*$/u', $heading); } private function normalizeWhitespace(string $string): string { return trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $string), " \n\r\t\x0C"); } } Link.php 0000644 00000001573 15112032673 0006160 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; /** * Link represents an HTML link (an HTML a, area or link tag). * * @author Fabien Potencier <fabien@symfony.com> */ class Link extends AbstractUriElement { protected function getRawUri(): string { return $this->node->getAttribute('href'); } /** * @return void */ protected function setNode(\DOMElement $node) { if ('a' !== $node->nodeName && 'area' !== $node->nodeName && 'link' !== $node->nodeName) { throw new \LogicException(sprintf('Unable to navigate from a "%s" tag.', $node->nodeName)); } $this->node = $node; } }