initial commit

This commit is contained in:
boris
2025-09-30 09:24:25 +01:00
committed by boris
parent a783a12c97
commit c7770ea03b
4695 changed files with 525784 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
<?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
{
protected \DOMElement $node;
protected ?string $method;
/**
* @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,
protected ?string $currentUri = null,
?string $method = 'GET',
) {
$this->setNode($node);
$this->method = $method ? strtoupper($method) : null;
$elementUriIsRelative = !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
*
* @throws \LogicException If given node is not an anchor
*/
abstract protected function setNode(\DOMElement $node): void;
}

130
vendor/symfony/dom-crawler/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,130 @@
CHANGELOG
=========
7.0
---
* Add argument `$normalizeWhitespace` to `Crawler::innerText()`
* Add argument `$default` to `Crawler::attr()`
6.4
---
* Add `CrawlerAnySelectorTextContains` test constraint
* Add `CrawlerAnySelectorTextSame` test constraint
* Add argument `$default` to `Crawler::attr()`
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

1237
vendor/symfony/dom-crawler/Crawler.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
<?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 ('checkbox' === $this->type) {
return parent::isDisabled();
}
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.
*/
public function select(string|array|bool $value): void
{
$this->setValue($value);
}
/**
* Ticks a checkbox.
*
* @throws \LogicException When the type provided is not correct
*/
public function tick(): void
{
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.
*
* @throws \LogicException When the type provided is not correct
*/
public function untick(): void
{
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.
*
* @throws \InvalidArgumentException When value type provided is not correct
*/
public function setValue(string|array|bool|null $value): void
{
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.
*
* @throws \LogicException When node type is incorrect
*/
protected function initialize(): void
{
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) && $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;
}
}

View File

@@ -0,0 +1,103 @@
<?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)
*
* @throws \InvalidArgumentException When error code doesn't exist
*/
public function setErrorCode(int $error): void
{
$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.
*/
public function upload(?string $value): void
{
$this->setValue($value);
}
/**
* Sets the value of the field.
*/
public function setValue(?string $value): void
{
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 = tempnam(sys_get_temp_dir(), $name);
if (\array_key_exists('extension', $info)) {
unlink($tmp);
$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.
*/
public function setFilePath(string $path): void
{
parent::setValue($path);
}
/**
* Initializes the form field.
*
* @throws \LogicException When node type is incorrect
*/
protected function initialize(): void
{
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);
}
}

View File

@@ -0,0 +1,102 @@
<?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
{
protected string $name;
protected string|array|null $value = null;
protected \DOMDocument $document;
protected \DOMXPath $xpath;
protected bool $disabled = false;
/**
* @param \DOMElement $node The node associated with this field
*/
public function __construct(
protected \DOMElement $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.
*/
public function setValue(?string $value): void
{
$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.
*/
abstract protected function initialize(): void;
}

View File

@@ -0,0 +1,46 @@
<?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.
*
* @throws \LogicException When node type is incorrect
*/
protected function initialize(): void
{
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');
}
}

View File

@@ -0,0 +1,37 @@
<?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.
*
* @throws \LogicException When node type is incorrect
*/
protected function initialize(): void
{
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;
}
}
}

463
vendor/symfony/dom-crawler/Form.php vendored Normal file
View File

@@ -0,0 +1,463 @@
<?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;
/**
* @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,
private ?string $baseHref = null,
) {
parent::__construct($node, $currentUri, $method);
$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 ($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 ($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'])) {
$currentParameters = [];
if ($query = parse_url($uri, \PHP_URL_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.
*/
public function remove(string $name): void
{
$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.
*/
public function set(FormField $field): void
{
$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 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.
*
* @throws \LogicException If given node is not a button or input or does not have a form ancestor
*/
protected function setNode(\DOMElement $node): void
{
$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) or ancestor::turbo-stream )]', $formId));
} 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) or ancestor::turbo-stream )]', $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 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 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));
}
}
}

View File

@@ -0,0 +1,175 @@
<?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 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 = $base ? \sprintf('%s[%s]', $base, $k) : $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];
}
}

37
vendor/symfony/dom-crawler/Image.php vendored Normal file
View File

@@ -0,0 +1,37 @@
<?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');
}
protected function setNode(\DOMElement $node): void
{
if ('img' !== $node->nodeName) {
throw new \LogicException(\sprintf('Unable to visualize a "%s" tag.', $node->nodeName));
}
$this->node = $node;
}
}

19
vendor/symfony/dom-crawler/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
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.

34
vendor/symfony/dom-crawler/Link.php vendored Normal file
View File

@@ -0,0 +1,34 @@
<?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');
}
protected function setNode(\DOMElement $node): void
{
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;
}
}

13
vendor/symfony/dom-crawler/README.md vendored Normal file
View File

@@ -0,0 +1,13 @@
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)

View File

@@ -0,0 +1,67 @@
<?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 CrawlerAnySelectorTextContains extends Constraint
{
private bool $hasNode = false;
public function __construct(
private string $selector,
private string $expectedText,
) {
}
public function toString(): string
{
if ($this->hasNode) {
return \sprintf('the text of any node matching selector "%s" contains "%s"', $this->selector, $this->expectedText);
}
return \sprintf('the Crawler has a node matching selector "%s"', $this->selector);
}
protected function matches($other): bool
{
if (!$other instanceof Crawler) {
throw new \InvalidArgumentException(\sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
}
$other = $other->filter($this->selector);
if (!\count($other)) {
$this->hasNode = false;
return false;
}
$this->hasNode = true;
$nodes = $other->each(fn (Crawler $node) => $node->text(null, true));
$matches = array_filter($nodes, function (string $node): bool {
return str_contains($node, $this->expectedText);
});
return 0 < \count($matches);
}
protected function failureDescription($other): string
{
if (!$other instanceof Crawler) {
throw new \InvalidArgumentException(\sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
}
return $this->toString();
}
}

View File

@@ -0,0 +1,54 @@
<?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 CrawlerAnySelectorTextSame extends Constraint
{
public function __construct(
private string $selector,
private string $expectedText,
) {
}
public function toString(): string
{
return \sprintf('has at least a node matching selector "%s" with content "%s"', $this->selector, $this->expectedText);
}
protected function matches($other): bool
{
if (!$other instanceof Crawler) {
throw new \InvalidArgumentException(\sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
}
$other = $other->filter($this->selector);
if (!\count($other)) {
return false;
}
$nodes = $other->each(fn (Crawler $node) => trim($node->text(null, true)));
return \in_array($this->expectedText, $nodes, true);
}
protected function failureDescription($other): string
{
if (!$other instanceof Crawler) {
throw new \InvalidArgumentException(\sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
}
return 'the Crawler '.$this->toString();
}
}

View File

@@ -0,0 +1,51 @@
<?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
{
public function __construct(
private string $selector,
private string $attribute,
private string $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();
}
}

View File

@@ -0,0 +1,45 @@
<?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)));
}
}

View File

@@ -0,0 +1,44 @@
<?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
{
public function __construct(
private string $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();
}
}

View File

@@ -0,0 +1,62 @@
<?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 bool $hasNode = false;
private string $nodeText;
public function __construct(
private string $selector,
private string $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();
}
}

View File

@@ -0,0 +1,50 @@
<?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
{
public function __construct(
private string $selector,
private string $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();
}
}

View File

@@ -0,0 +1,136 @@
<?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(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $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;
}
}

View File

@@ -0,0 +1,34 @@
{
"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.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0",
"masterminds/html5": "^2.6"
},
"require-dev": {
"symfony/css-selector": "^6.4|^7.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\DomCrawler\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}