diff options
Diffstat (limited to 'vendor/league')
51 files changed, 7599 insertions, 0 deletions
diff --git a/vendor/league/uri-interfaces/Contracts/AuthorityInterface.php b/vendor/league/uri-interfaces/Contracts/AuthorityInterface.php new file mode 100644 index 000000000..9c364f33e --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/AuthorityInterface.php @@ -0,0 +1,93 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use League\Uri\Exceptions\MissingFeature; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +interface AuthorityInterface extends UriComponentInterface +{ + /** + * Returns the host component of the authority. + */ + public function getHost(): ?string; + + /** + * Returns the port component of the authority. + */ + public function getPort(): ?int; + + /** + * Returns the user information component of the authority. + */ + public function getUserInfo(): ?string; + + /** + * Returns an associative array containing all the Authority components. + * + * The returned a hashmap similar to PHP's parse_url return value + * + * @link https://tools.ietf.org/html/rfc3986 + * + * @return array{user: ?string, pass : ?string, host: ?string, port: ?int} + */ + public function components(): array; + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * A null value provided for the host is equivalent to removing the host + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + * @throws MissingFeature for component or transformations + * requiring IDN support when IDN support is not present + * or misconfigured. + */ + public function withHost(Stringable|string|null $host): self; + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withPort(?int $port): self; + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; a null value for the user is equivalent to removing user + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/DataPathInterface.php b/vendor/league/uri-interfaces/Contracts/DataPathInterface.php new file mode 100644 index 000000000..01f9c402a --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/DataPathInterface.php @@ -0,0 +1,95 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use SplFileObject; +use Stringable; + +interface DataPathInterface extends PathInterface +{ + /** + * Retrieve the data mime type associated to the URI. + * + * If no mimetype is present, this method MUST return the default mimetype 'text/plain'. + * + * @see http://tools.ietf.org/html/rfc2397#section-2 + */ + public function getMimeType(): string; + + /** + * Retrieve the parameters associated with the Mime Type of the URI. + * + * If no parameters is present, this method MUST return the default parameter 'charset=US-ASCII'. + * + * @see http://tools.ietf.org/html/rfc2397#section-2 + */ + public function getParameters(): string; + + /** + * Retrieve the mediatype associated with the URI. + * + * If no mediatype is present, this method MUST return the default parameter 'text/plain;charset=US-ASCII'. + * + * @see http://tools.ietf.org/html/rfc2397#section-3 + * + * @return string The URI scheme. + */ + public function getMediaType(): string; + + /** + * Retrieves the data string. + * + * Retrieves the data part of the path. If no data part is provided return + * an empty string + */ + public function getData(): string; + + /** + * Tells whether the data is binary safe encoded. + */ + public function isBinaryData(): bool; + + /** + * Save the data to a specific file. + */ + public function save(string $path, string $mode = 'w'): SplFileObject; + + /** + * Returns an instance where the data part is base64 encoded. + * + * This method MUST retain the state of the current instance, and return + * an instance where the data part is base64 encoded + */ + public function toBinary(): self; + + /** + * Returns an instance where the data part is url encoded following RFC3986 rules. + * + * This method MUST retain the state of the current instance, and return + * an instance where the data part is url encoded + */ + public function toAscii(): self; + + /** + * Return an instance with the specified mediatype parameters. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified mediatype parameters. + * + * Users must provide encoded characters. + * + * An empty parameters value is equivalent to removing the parameter. + */ + public function withParameters(Stringable|string $parameters): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php b/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php new file mode 100644 index 000000000..66758044d --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php @@ -0,0 +1,117 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use Countable; +use Iterator; +use IteratorAggregate; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +/** + * @extends IteratorAggregate<string> + */ +interface DomainHostInterface extends Countable, HostInterface, IteratorAggregate +{ + /** + * Returns the labels total number. + */ + public function count(): int; + + /** + * Iterate over the Domain labels. + * + * @return Iterator<string> + */ + public function getIterator(): Iterator; + + /** + * Retrieves a single host label. + * + * If the label offset has not been set, returns the null value. + */ + public function get(int $offset): ?string; + + /** + * Returns the associated key for a specific label or all the keys. + * + * @return int[] + */ + public function keys(?string $label = null): array; + + /** + * Tells whether the domain is absolute. + */ + public function isAbsolute(): bool; + + /** + * Prepends a label to the host. + */ + public function prepend(Stringable|string $label): self; + + /** + * Appends a label to the host. + */ + public function append(Stringable|string $label): self; + + /** + * Extracts a slice of $length elements starting at position $offset from the host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the selected slice. + * + * If $length is null it returns all elements from $offset to the end of the Domain. + */ + public function slice(int $offset, ?int $length = null): self; + + /** + * Returns an instance with its Root label. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2.2 + */ + public function withRootLabel(): self; + + /** + * Returns an instance without its Root label. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2.2 + */ + public function withoutRootLabel(): self; + + /** + * Returns an instance with the modified label. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the new label + * + * If $key is non-negative, the added label will be the label at $key position from the start. + * If $key is negative, the added label will be the label at $key position from the end. + * + * @throws SyntaxError If the key is invalid + */ + public function withLabel(int $key, Stringable|string $label): self; + + /** + * Returns an instance without the specified label. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified component + * + * If $key is non-negative, the removed label will be the label at $key position from the start. + * If $key is negative, the removed label will be the label at $key position from the end. + * + * @throws SyntaxError If the key is invalid + */ + public function withoutLabel(int ...$keys): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/FragmentInterface.php b/vendor/league/uri-interfaces/Contracts/FragmentInterface.php new file mode 100644 index 000000000..3d80f0661 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/FragmentInterface.php @@ -0,0 +1,22 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +interface FragmentInterface extends UriComponentInterface +{ + /** + * Returns the decoded fragment. + */ + public function decoded(): ?string; +} diff --git a/vendor/league/uri-interfaces/Contracts/HostInterface.php b/vendor/league/uri-interfaces/Contracts/HostInterface.php new file mode 100644 index 000000000..16212bfe1 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/HostInterface.php @@ -0,0 +1,56 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +interface HostInterface extends UriComponentInterface +{ + /** + * Returns the ascii representation. + */ + public function toAscii(): ?string; + + /** + * Returns the unicode representation. + */ + public function toUnicode(): ?string; + + /** + * Returns the IP version. + * + * If the host is a not an IP this method will return null + */ + public function getIpVersion(): ?string; + + /** + * Returns the IP component If the Host is an IP address. + * + * If the host is a not an IP this method will return null + */ + public function getIp(): ?string; + + /** + * Tells whether the host is a domain name. + */ + public function isDomain(): bool; + + /** + * Tells whether the host is an IP Address. + */ + public function isIp(): bool; + + /** + * Tells whether the host is a registered name. + */ + public function isRegisteredName(): bool; +} diff --git a/vendor/league/uri-interfaces/Contracts/IpHostInterface.php b/vendor/league/uri-interfaces/Contracts/IpHostInterface.php new file mode 100644 index 000000000..7daac22b3 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/IpHostInterface.php @@ -0,0 +1,49 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +interface IpHostInterface extends HostInterface +{ + /** + * Tells whether the host is an IPv4 address. + */ + public function isIpv4(): bool; + + /** + * Tells whether the host is an IPv6 address. + */ + public function isIpv6(): bool; + + /** + * Tells whether the host is an IPv6 address. + */ + public function isIpFuture(): bool; + + /** + * Tells whether the host has a ZoneIdentifier. + * + * @see http://tools.ietf.org/html/rfc6874#section-4 + */ + public function hasZoneIdentifier(): bool; + + /** + * Returns a host without its zone identifier according to RFC6874. + * + * This method MUST retain the state of the current instance, and return + * an instance without the host zone identifier according to RFC6874 + * + * @see http://tools.ietf.org/html/rfc6874#section-4 + */ + public function withoutZoneIdentifier(): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/PathInterface.php b/vendor/league/uri-interfaces/Contracts/PathInterface.php new file mode 100644 index 000000000..f99b76270 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/PathInterface.php @@ -0,0 +1,90 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use League\Uri\Exceptions\SyntaxError; + +interface PathInterface extends UriComponentInterface +{ + /** + * Returns the decoded path. + */ + public function decoded(): string; + + /** + * Tells whether the path is absolute or relative. + */ + public function isAbsolute(): bool; + + /** + * Tells whether the path has a trailing slash. + */ + public function hasTrailingSlash(): bool; + + /** + * Returns an instance without dot segments. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the path component normalized by removing + * the dot segment. + * + * @throws SyntaxError for invalid component or transformations + * that would result in a object in invalid state. + */ + public function withoutDotSegments(): self; + + /** + * Returns an instance with a leading slash. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the path component with a leading slash + * + * @throws SyntaxError for invalid component or transformations + * that would result in a object in invalid state. + */ + public function withLeadingSlash(): self; + + /** + * Returns an instance without a leading slash. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the path component without a leading slash + * + * @throws SyntaxError for invalid component or transformations + * that would result in a object in invalid state. + */ + public function withoutLeadingSlash(): self; + + /** + * Returns an instance with a trailing slash. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the path component with a trailing slash + * + * @throws SyntaxError for invalid component or transformations + * that would result in a object in invalid state. + */ + public function withTrailingSlash(): self; + + /** + * Returns an instance without a trailing slash. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the path component without a trailing slash + * + * @throws SyntaxError for invalid component or transformations + * that would result in a object in invalid state. + */ + public function withoutTrailingSlash(): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/PortInterface.php b/vendor/league/uri-interfaces/Contracts/PortInterface.php new file mode 100644 index 000000000..7230c4ad1 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/PortInterface.php @@ -0,0 +1,22 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +interface PortInterface extends UriComponentInterface +{ + /** + * Returns the integer representation of the Port. + */ + public function toInt(): ?int; +} diff --git a/vendor/league/uri-interfaces/Contracts/QueryInterface.php b/vendor/league/uri-interfaces/Contracts/QueryInterface.php new file mode 100644 index 000000000..fed486e34 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/QueryInterface.php @@ -0,0 +1,253 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use Countable; +use Deprecated; +use Iterator; +use IteratorAggregate; +use Stringable; + +/** + * @extends IteratorAggregate<array{0:string, 1:string|null}> + * + * @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys. + * @method self withoutPairByValue(Stringable|string|int|bool|null ...$values) Returns an instance without pairs with the specified values. + * @method self withoutPairByKeyValue(string $key, Stringable|string|int|bool|null $value) Returns an instance without pairs with the specified key/value pair + * @method bool hasPair(string $key, ?string $value) Tells whether the pair exists in the query. + * @method ?string toFormData() Returns the string representation using the applicat/www-form-urlencoded rules + * @method ?string toRFC3986() Returns the string representation using RFC3986 rules + */ +interface QueryInterface extends Countable, IteratorAggregate, UriComponentInterface +{ + /** + * Returns the query separator. + * + * @return non-empty-string + */ + public function getSeparator(): string; + + /** + * Returns the number of key/value pairs present in the object. + */ + public function count(): int; + + /** + * Returns an iterator allowing to go through all key/value pairs contained in this object. + * + * The pair is represented as an array where the first value is the pair key + * and the second value the pair value. + * + * The key of each pair is a string + * The value of each pair is a scalar or the null value + * + * @return Iterator<int, array{0:string, 1:string|null}> + */ + public function getIterator(): Iterator; + + /** + * Returns an iterator allowing to go through all key/value pairs contained in this object. + * + * The return type is as an Iterator where its offset is the pair key and its value the pair value. + * + * The key of each pair is a string + * The value of each pair is a scalar or the null value + * + * @return iterable<string, string|null> + */ + public function pairs(): iterable; + + /** + * Tells whether a list of pair with a specific key exists. + * + * @see https://url.spec.whatwg.org/#dom-urlsearchparams-has + */ + public function has(string ...$keys): bool; + + /** + * Returns the first value associated to the given pair name. + * + * If no value is found null is returned + * + * @see https://url.spec.whatwg.org/#dom-urlsearchparams-get + */ + public function get(string $key): ?string; + + /** + * Returns all the values associated to the given pair name as an array or all + * the instance pairs. + * + * If no value is found an empty array is returned + * + * @see https://url.spec.whatwg.org/#dom-urlsearchparams-getall + * + * @return array<int, string|null> + */ + public function getAll(string $key): array; + + /** + * Returns the store PHP variables as elements of an array. + * + * The result is similar as PHP parse_str when used with its + * second argument with the difference that variable names are + * not mangled. + * + * @see http://php.net/parse_str + * @see https://wiki.php.net/rfc/on_demand_name_mangling + * + * @return array the collection of stored PHP variables or the empty array if no input is given, + */ + public function parameters(): array; + + /** + * Returns the value attached to the specific key. + * + * The result is similar to PHP parse_str with the difference that variable + * names are not mangled. + * + * If a key is submitted it will return the value attached to it or null + * + * @see http://php.net/parse_str + * @see https://wiki.php.net/rfc/on_demand_name_mangling + * + * @return mixed the collection of stored PHP variables or the empty array if no input is given, + * the single value of a stored PHP variable or null if the variable is not present in the collection + */ + public function parameter(string $name): mixed; + + /** + * Tells whether a list of variable with specific names exists. + * + * @see https://url.spec.whatwg.org/#dom-urlsearchparams-has + */ + public function hasParameter(string ...$names): bool; + + /** + * Returns the RFC1738 encoded query. + */ + public function toRFC1738(): ?string; + + /** + * Returns an instance with a different separator. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the query component with a different separator + */ + public function withSeparator(string $separator): self; + + /** + * Returns an instance with the new pairs set to it. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified query + * + * @see ::withPair + */ + public function merge(Stringable|string $query): self; + + /** + * Returns an instance with the new pairs appended to it. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified query + * + * If the pair already exists the value will be added to it. + */ + public function append(Stringable|string $query): self; + + /** + * Returns a new instance with a specified key/value pair appended as a new pair. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified query + */ + public function appendTo(string $key, Stringable|string|int|bool|null $value): self; + + /** + * Sorts the query string by offset, maintaining offset to data correlations. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified query + * + * @see https://url.spec.whatwg.org/#dom-urlsearchparams-sort + */ + public function sort(): self; + + /** + * Returns an instance without duplicate key/value pair. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the query component normalized by removing + * duplicate pairs whose key/value are the same. + */ + public function withoutDuplicates(): self; + + /** + * Returns an instance without empty key/value where the value is the null value. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the query component normalized by removing + * empty pairs. + * + * A pair is considered empty if its value is equal to the null value + */ + public function withoutEmptyPairs(): self; + + /** + * Returns an instance where numeric indices associated to PHP's array like key are removed. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the query component normalized so that numeric indexes + * are removed from the pair key value. + * + * i.e.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar + */ + public function withoutNumericIndices(): self; + + /** + * Returns an instance with a new key/value pair added to it. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified query + * + * If the pair already exists the value will replace the existing value. + * + * @see https://url.spec.whatwg.org/#dom-urlsearchparams-set + */ + public function withPair(string $key, Stringable|string|int|float|bool|null $value): self; + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.3.0 + * @codeCoverageIgnore + * @see QueryInterface::withoutPairByKey() + * + * Returns an instance without the specified keys. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified component + */ + #[Deprecated(message:'use League\Uri\Contracts\QueryInterface::withoutPairByKey() instead', since:'league/uri-interfaces:7.3.0')] + public function withoutPair(string ...$keys): self; + + /** + * Returns an instance without the specified params. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified component without PHP's value. + * PHP's mangled is not taken into account. + */ + public function withoutParameters(string ...$names): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/SegmentedPathInterface.php b/vendor/league/uri-interfaces/Contracts/SegmentedPathInterface.php new file mode 100644 index 000000000..fa5a78d22 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/SegmentedPathInterface.php @@ -0,0 +1,149 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use Countable; +use Iterator; +use IteratorAggregate; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +/** + * @extends IteratorAggregate<string> + */ +interface SegmentedPathInterface extends Countable, IteratorAggregate, PathInterface +{ + /** + * Returns the total number of segments in the path. + */ + public function count(): int; + + /** + * Iterate over the path segment. + * + * @return Iterator<string> + */ + public function getIterator(): Iterator; + + /** + * Returns parent directory's path. + */ + public function getDirname(): string; + + /** + * Returns the path basename. + */ + public function getBasename(): string; + + /** + * Returns the basename extension. + */ + public function getExtension(): string; + + /** + * Retrieves a single path segment. + * + * If the segment offset has not been set, returns null. + */ + public function get(int $offset): ?string; + + /** + * Returns the associated key for a specific segment. + * + * If a value is specified only the keys associated with + * the given value will be returned + * + * @return array<int> + */ + public function keys(Stringable|string|null $segment = null): array; + + /** + * Appends a segment to the path. + */ + public function append(Stringable|string $segment): self; + + /** + * Extracts a slice of $length elements starting at position $offset from the host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the selected slice. + * + * If $length is null it returns all elements from $offset to the end of the Path. + */ + public function slice(int $offset, ?int $length = null): self; + + /** + * Prepends a segment to the path. + */ + public function prepend(Stringable|string $segment): self; + + /** + * Returns an instance with the modified segment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the new segment + * + * If $key is non-negative, the added segment will be the segment at $key position from the start. + * If $key is negative, the added segment will be the segment at $key position from the end. + * + * @throws SyntaxError If the key is invalid + */ + public function withSegment(int $key, Stringable|string $segment): self; + + /** + * Returns an instance without the specified segment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified component + * + * If $key is non-negative, the removed segment will be the segment at $key position from the start. + * If $key is negative, the removed segment will be the segment at $key position from the end. + * + * @throws SyntaxError If the key is invalid + */ + public function withoutSegment(int ...$keys): self; + + /** + * Returns an instance without duplicate delimiters. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the path component normalized by removing + * multiple consecutive empty segment + */ + public function withoutEmptySegments(): self; + + /** + * Returns an instance with the specified parent directory's path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the extension basename modified. + */ + public function withDirname(Stringable|string $path): self; + + /** + * Returns an instance with the specified basename. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the extension basename modified. + */ + public function withBasename(Stringable|string $basename): self; + + /** + * Returns an instance with the specified basename extension. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the extension basename modified. + */ + public function withExtension(Stringable|string $extension): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/UriAccess.php b/vendor/league/uri-interfaces/Contracts/UriAccess.php new file mode 100644 index 000000000..7c37cdadb --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/UriAccess.php @@ -0,0 +1,26 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use Psr\Http\Message\UriInterface as Psr7UriInterface; + +interface UriAccess +{ + public function getUri(): UriInterface|Psr7UriInterface; + + /** + * Returns the RFC3986 string representation of the complete URI. + */ + public function getUriString(): string; +} diff --git a/vendor/league/uri-interfaces/Contracts/UriComponentInterface.php b/vendor/league/uri-interfaces/Contracts/UriComponentInterface.php new file mode 100644 index 000000000..e478516e8 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/UriComponentInterface.php @@ -0,0 +1,75 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use JsonSerializable; +use Stringable; + +interface UriComponentInterface extends JsonSerializable, Stringable +{ + /** + * Returns the instance string representation. + * + * If the instance is defined, the value returned MUST be percent-encoded, + * but MUST NOT double-encode any characters. To determine what characters + * to encode, please refer to RFC 3986, Sections 2 and 3. + * + * If the instance is not defined null is returned + */ + public function value(): ?string; + + /** + * Returns the instance string representation. + * + * If the instance is defined, the value returned MUST be percent-encoded, + * but MUST NOT double-encode any characters. To determine what characters + * to encode, please refer to RFC 3986, Sections 2 and 3. + * + * If the instance is not defined an empty string is returned + */ + public function toString(): string; + + /** + * Returns the instance string representation. + * + * If the instance is defined, the value returned MUST be percent-encoded, + * but MUST NOT double-encode any characters. To determine what characters + * to encode, please refer to RFC 3986, Sections 2 and 3. + * + * If the instance is not defined an empty string is returned + */ + public function __toString(): string; + + /** + * Returns the instance json representation. + * + * If the instance is defined, the value returned MUST be percent-encoded, + * but MUST NOT double-encode any characters. To determine what characters + * to encode, please refer to RFC 3986 or RFC 1738. + * + * If the instance is not defined null is returned + */ + public function jsonSerialize(): ?string; + + /** + * Returns the instance string representation with its optional URI delimiters. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode any + * characters. To determine what characters to encode, please refer to RFC 3986, + * Sections 2 and 3. + * + * If the instance is not defined an empty string is returned + */ + public function getUriComponent(): string; +} diff --git a/vendor/league/uri-interfaces/Contracts/UriException.php b/vendor/league/uri-interfaces/Contracts/UriException.php new file mode 100644 index 000000000..c0fec2a12 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/UriException.php @@ -0,0 +1,20 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use Throwable; + +interface UriException extends Throwable +{ +} diff --git a/vendor/league/uri-interfaces/Contracts/UriInterface.php b/vendor/league/uri-interfaces/Contracts/UriInterface.php new file mode 100644 index 000000000..1fde6b966 --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/UriInterface.php @@ -0,0 +1,314 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use JsonSerializable; +use League\Uri\Exceptions\MissingFeature; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\UriString; +use Stringable; + +/** + * @phpstan-import-type ComponentMap from UriString + * + * @method string|null getUsername() returns the user component of the URI. + * @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource. + * @method array toComponents() returns an associative array containing all the URI components. + */ +interface UriInterface extends JsonSerializable, Stringable +{ + /** + * Returns the string representation as a URI reference. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + */ + public function __toString(): string; + + /** + * Returns the string representation as a URI reference. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + */ + public function toString(): string; + + /** + * Returns the string representation as a URI reference. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @see ::__toString + */ + public function jsonSerialize(): string; + + /** + * Retrieve the scheme component of the URI. + * + * If no scheme is present, this method MUST return a null value. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.1. + * + * The trailing ":" character is not part of the scheme and MUST NOT be + * added. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.1 + */ + public function getScheme(): ?string; + + /** + * Retrieve the authority component of the URI. + * + * If no scheme is present, this method MUST return a null value. + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + */ + public function getAuthority(): ?string; + + /** + * Retrieve the user information component of the URI. + * + * If no scheme is present, this method MUST return a null value. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + */ + public function getUserInfo(): ?string; + + /** + * Retrieve the host component of the URI. + * + * If no host is present this method MUST return a null value. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + */ + public function getHost(): ?string; + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + */ + public function getPort(): ?int; + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + */ + public function getPath(): string; + + /** + * Retrieve the query string of the URI. + * + * If no host is present this method MUST return a null value. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + */ + public function getQuery(): ?string; + + /** + * Retrieve the fragment component of the URI. + * + * If no host is present this method MUST return a null value. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + */ + public function getFragment(): ?string; + + /** + * Returns an associative array containing all the URI components. + * + * The returned array is similar to PHP's parse_url return value with the following + * differences: + * + * <ul> + * <li>All components are present in the returned array</li> + * <li>Empty and undefined component are treated differently. And empty component is + * set to the empty string while an undefined component is set to the `null` value.</li> + * </ul> + * + * @link https://tools.ietf.org/html/rfc3986 + * + * @return ComponentMap + */ + public function getComponents(): array; + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * A null value provided for the scheme is equivalent to removing the scheme + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withScheme(Stringable|string|null $scheme): self; + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; a null value for the user is equivalent to removing user + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self; + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * A null value provided for the host is equivalent to removing the host + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + * @throws MissingFeature for component or transformations + * requiring IDN support when IDN support is not present + * or misconfigured. + */ + public function withHost(Stringable|string|null $host): self; + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withPort(?int $port): self; + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withPath(Stringable|string $path): self; + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * A null value provided for the query is equivalent to removing the query + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withQuery(Stringable|string|null $query): self; + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * A null value provided for the fragment is equivalent to removing the fragment + * information. + * + * @throws SyntaxError for invalid component or transformations + * that would result in an object in invalid state. + */ + public function withFragment(Stringable|string|null $fragment): self; +} diff --git a/vendor/league/uri-interfaces/Contracts/UserInfoInterface.php b/vendor/league/uri-interfaces/Contracts/UserInfoInterface.php new file mode 100644 index 000000000..bd36b86de --- /dev/null +++ b/vendor/league/uri-interfaces/Contracts/UserInfoInterface.php @@ -0,0 +1,62 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use Stringable; + +interface UserInfoInterface extends UriComponentInterface +{ + /** + * Returns the user component part. + */ + public function getUser(): ?string; + + /** + * Returns the pass component part. + */ + public function getPass(): ?string; + + /** + * Returns an associative array containing all the User Info components. + * + * The returned a hashmap similar to PHP's parse_url return value + * + * @link https://tools.ietf.org/html/rfc3986 + * + * @return array{user: ?string, pass : ?string} + */ + public function components(): array; + + /** + * Returns an instance with the specified user and/or pass. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified new username + * otherwise it returns the same instance unchanged. + * + * A variable equal to null is equivalent to removing the complete user information. + */ + public function withUser(Stringable|string|null $username): self; + + /** + * Returns an instance with the specified user and/or pass. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified password if the user is specified + * otherwise it returns the same instance unchanged. + * + * An empty user is equivalent to removing the user information. + */ + public function withPass(Stringable|string|null $password): self; +} diff --git a/vendor/league/uri-interfaces/Encoder.php b/vendor/league/uri-interfaces/Encoder.php new file mode 100644 index 000000000..4324e03c8 --- /dev/null +++ b/vendor/league/uri-interfaces/Encoder.php @@ -0,0 +1,176 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use Closure; +use League\Uri\Contracts\UriComponentInterface; +use League\Uri\Exceptions\SyntaxError; +use SensitiveParameter; +use Stringable; + +use function preg_match; +use function preg_replace_callback; +use function rawurldecode; +use function rawurlencode; +use function strtoupper; + +final class Encoder +{ + private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/'; + private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},'; + private const REGEXP_CHARS_PREVENTS_DECODING = ',% + 2[A-F|1-2|4-9]| + 3[0-9|B|D]| + 4[1-9|A-F]| + 5[0-9|A|F]| + 6[1-9|A-F]| + 7[0-9|E] + ,ix'; + private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%"; + private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~'; + private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})'; + + /** + * Encode User. + * + * All generic delimiters MUST be encoded + */ + public static function encodeUser(Stringable|string|null $component): ?string + { + static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/'; + + return self::encode($component, $pattern); + } + + /** + * Encode Password. + * + * Generic delimiters ":" MUST NOT be encoded + */ + public static function encodePassword(#[SensitiveParameter] Stringable|string|null $component): ?string + { + static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/'; + + return self::encode($component, $pattern); + } + + /** + * Encode Path. + * + * Generic delimiters ":", "@", and "/" MUST NOT be encoded + */ + public static function encodePath(Stringable|string|null $component): string + { + static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/'; + + return (string) self::encode($component, $pattern); + } + + /** + * Encode Query or Fragment. + * + * Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded + */ + public static function encodeQueryOrFragment(Stringable|string|null $component): ?string + { + static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?]+|'.self::REGEXP_PART_ENCODED.'/'; + + return self::encode($component, $pattern); + } + + public static function encodeQueryKeyValue(mixed $component): ?string + { + static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/'; + + $encodeMatches = static fn (array $matches): string => match (1) { + preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]), + default => $matches[0], + }; + + $component = self::filterComponent($component); + + return match (true) { + !is_scalar($component) => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))), + 1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => rawurlencode($component), + 1 === preg_match($pattern, $component) => (string) preg_replace_callback($pattern, $encodeMatches(...), $component), + default => $component, + }; + } + + /** + * Decodes the URI component without decoding the unreserved characters which are already encoded. + */ + public static function decodePartial(Stringable|string|int|null $component): ?string + { + $decodeMatches = static fn (array $matches): string => match (1) { + preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0]) => strtoupper($matches[0]), + default => rawurldecode($matches[0]), + }; + + return self::decode($component, $decodeMatches); + } + + /** + * Decodes all the URI component characters. + */ + public static function decodeAll(Stringable|string|int|null $component): ?string + { + $decodeMatches = static fn (array $matches): string => rawurldecode($matches[0]); + + return self::decode($component, $decodeMatches); + } + + private static function filterComponent(mixed $component): ?string + { + return match (true) { + true === $component => '1', + false === $component => '0', + $component instanceof UriComponentInterface => $component->value(), + $component instanceof Stringable, + is_scalar($component) => (string) $component, + null === $component => null, + default => throw new SyntaxError(sprintf('The component must be a scalar value `%s` given.', gettype($component))), + }; + } + + private static function encode(Stringable|string|int|bool|null $component, string $pattern): ?string + { + $component = self::filterComponent($component); + $encodeMatches = static fn (array $matches): string => match (1) { + preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]), + default => $matches[0], + }; + + return match (true) { + null === $component, + '' === $component => $component, + default => (string) preg_replace_callback($pattern, $encodeMatches(...), $component), + }; + } + + /** + * Decodes all the URI component characters. + */ + private static function decode(Stringable|string|int|null $component, Closure $decodeMatches): ?string + { + $component = self::filterComponent($component); + + return match (true) { + null === $component => null, + 1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => throw new SyntaxError('Invalid component string: '.$component.'.'), + 1 === preg_match(self::REGEXP_CHARS_ENCODED, $component) => preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decodeMatches(...), $component), + default => $component, + }; + } +} diff --git a/vendor/league/uri-interfaces/Exceptions/ConversionFailed.php b/vendor/league/uri-interfaces/Exceptions/ConversionFailed.php new file mode 100644 index 000000000..973ffb320 --- /dev/null +++ b/vendor/league/uri-interfaces/Exceptions/ConversionFailed.php @@ -0,0 +1,46 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Exceptions; + +use League\Uri\Idna\Error; +use League\Uri\Idna\Result; +use Stringable; + +final class ConversionFailed extends SyntaxError +{ + private function __construct( + string $message, + private readonly string $host, + private readonly Result $result + ) { + parent::__construct($message); + } + + public static function dueToIdnError(Stringable|string $host, Result $result): self + { + $reasons = array_map(fn (Error $error): string => $error->description(), $result->errors()); + + return new self('Host `'.$host.'` is invalid: '.implode('; ', $reasons).'.', (string) $host, $result); + } + + public function getHost(): string + { + return $this->host; + } + + public function getResult(): Result + { + return $this->result; + } +} diff --git a/vendor/league/uri-interfaces/Exceptions/MissingFeature.php b/vendor/league/uri-interfaces/Exceptions/MissingFeature.php new file mode 100644 index 000000000..034e969fc --- /dev/null +++ b/vendor/league/uri-interfaces/Exceptions/MissingFeature.php @@ -0,0 +1,21 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Exceptions; + +use League\Uri\Contracts\UriException; +use RuntimeException; + +class MissingFeature extends RuntimeException implements UriException +{ +} diff --git a/vendor/league/uri-interfaces/Exceptions/OffsetOutOfBounds.php b/vendor/league/uri-interfaces/Exceptions/OffsetOutOfBounds.php new file mode 100644 index 000000000..737a5cc24 --- /dev/null +++ b/vendor/league/uri-interfaces/Exceptions/OffsetOutOfBounds.php @@ -0,0 +1,18 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Exceptions; + +class OffsetOutOfBounds extends SyntaxError +{ +} diff --git a/vendor/league/uri-interfaces/Exceptions/SyntaxError.php b/vendor/league/uri-interfaces/Exceptions/SyntaxError.php new file mode 100644 index 000000000..f44cca6c9 --- /dev/null +++ b/vendor/league/uri-interfaces/Exceptions/SyntaxError.php @@ -0,0 +1,21 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Exceptions; + +use InvalidArgumentException; +use League\Uri\Contracts\UriException; + +class SyntaxError extends InvalidArgumentException implements UriException +{ +} diff --git a/vendor/league/uri-interfaces/FeatureDetection.php b/vendor/league/uri-interfaces/FeatureDetection.php new file mode 100644 index 000000000..b3e9b09cd --- /dev/null +++ b/vendor/league/uri-interfaces/FeatureDetection.php @@ -0,0 +1,56 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use finfo; +use League\Uri\Exceptions\MissingFeature; +use League\Uri\IPv4\Calculator; + +use const PHP_INT_SIZE; + +/** + * Allow detecting features needed to make the packages work. + */ +final class FeatureDetection +{ + public static function supportsFileDetection(): void + { + static $isSupported = null; + $isSupported = $isSupported ?? class_exists(finfo::class); + + if (!$isSupported) { + throw new MissingFeature('Support for file type detection requires the `fileinfo` extension.'); + } + } + + public static function supportsIdn(): void + { + static $isSupported = null; + $isSupported = $isSupported ?? (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46')); + + if (!$isSupported) { + throw new MissingFeature('Support for IDN host requires the `intl` extension for best performance or run "composer require symfony/polyfill-intl-idn" to install a polyfill.'); + } + } + + public static function supportsIPv4Conversion(): void + { + static $isSupported = null; + $isSupported = $isSupported ?? (extension_loaded('gmp') || extension_loaded('bcmath') || (4 < PHP_INT_SIZE)); + + if (!$isSupported) { + throw new MissingFeature('A '.Calculator::class.' implementation could not be automatically loaded. To perform IPv4 conversion use a x.64 PHP build or install one of the following extension GMP or BCMath. You can also ship your own implmentation.'); + } + } +} diff --git a/vendor/league/uri-interfaces/IPv4/BCMathCalculator.php b/vendor/league/uri-interfaces/IPv4/BCMathCalculator.php new file mode 100644 index 000000000..b12ac9954 --- /dev/null +++ b/vendor/league/uri-interfaces/IPv4/BCMathCalculator.php @@ -0,0 +1,85 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\IPv4; + +use function bcadd; +use function bccomp; +use function bcdiv; +use function bcmod; +use function bcmul; +use function bcpow; +use function bcsub; +use function str_split; + +final class BCMathCalculator implements Calculator +{ + private const SCALE = 0; + private const CONVERSION_TABLE = [ + '0' => '0', '1' => '1', '2' => '2', '3' => '3', + '4' => '4', '5' => '5', '6' => '6', '7' => '7', + '8' => '8', '9' => '9', 'a' => '10', 'b' => '11', + 'c' => '12', 'd' => '13', 'e' => '14', 'f' => '15', + ]; + + public function baseConvert(mixed $value, int $base): string + { + $value = (string) $value; + if (10 === $base) { + return $value; + } + + $base = (string) $base; + $decimal = '0'; + foreach (str_split($value) as $char) { + $decimal = bcadd($this->multiply($decimal, $base), self::CONVERSION_TABLE[$char], self::SCALE); + } + + return $decimal; + } + + public function pow(mixed $value, int $exponent): string + { + return bcpow((string) $value, (string) $exponent, self::SCALE); + } + + public function compare(mixed $value1, $value2): int + { + return bccomp((string) $value1, (string) $value2, self::SCALE); + } + + public function multiply(mixed $value1, $value2): string + { + return bcmul((string) $value1, (string) $value2, self::SCALE); + } + + public function div(mixed $value, mixed $base): string + { + return bcdiv((string) $value, (string) $base, self::SCALE); + } + + public function mod(mixed $value, mixed $base): string + { + return bcmod((string) $value, (string) $base, self::SCALE); + } + + public function add(mixed $value1, mixed $value2): string + { + return bcadd((string) $value1, (string) $value2, self::SCALE); + } + + public function sub(mixed $value1, mixed $value2): string + { + return bcsub((string) $value1, (string) $value2, self::SCALE); + } +} diff --git a/vendor/league/uri-interfaces/IPv4/Calculator.php b/vendor/league/uri-interfaces/IPv4/Calculator.php new file mode 100644 index 000000000..78a3c333b --- /dev/null +++ b/vendor/league/uri-interfaces/IPv4/Calculator.php @@ -0,0 +1,95 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\IPv4; + +interface Calculator +{ + /** + * Add numbers. + * + * @param mixed $value1 a number that will be added to $value2 + * @param mixed $value2 a number that will be added to $value1 + * + * @return mixed the addition result + */ + public function add(mixed $value1, mixed $value2); + + /** + * Subtract one number from another. + * + * @param mixed $value1 a number that will be subtracted of $value2 + * @param mixed $value2 a number that will be subtracted to $value1 + * + * @return mixed the subtraction result + */ + public function sub(mixed $value1, mixed $value2); + + /** + * Multiply numbers. + * + * @param mixed $value1 a number that will be multiplied by $value2 + * @param mixed $value2 a number that will be multiplied by $value1 + * + * @return mixed the multiplication result + */ + public function multiply(mixed $value1, mixed $value2); + + /** + * Divide numbers. + * + * @param mixed $value The number being divided. + * @param mixed $base The number that $value is being divided by. + * + * @return mixed the result of the division + */ + public function div(mixed $value, mixed $base); + + /** + * Raise an number to the power of exponent. + * + * @param mixed $value scalar, the base to use + * + * @return mixed the value raised to the power of exp. + */ + public function pow(mixed $value, int $exponent); + + /** + * Returns the int point remainder (modulo) of the division of the arguments. + * + * @param mixed $value The dividend + * @param mixed $base The divisor + * + * @return mixed the remainder + */ + public function mod(mixed $value, mixed $base); + + /** + * Number comparison. + * + * @param mixed $value1 the first value + * @param mixed $value2 the second value + * + * @return int Returns < 0 if value1 is less than value2; > 0 if value1 is greater than value2, and 0 if they are equal. + */ + public function compare(mixed $value1, mixed $value2): int; + + /** + * Get the decimal integer value of a variable. + * + * @param mixed $value The scalar value being converted to an integer + * + * @return mixed the integer value + */ + public function baseConvert(mixed $value, int $base); +} diff --git a/vendor/league/uri-interfaces/IPv4/Converter.php b/vendor/league/uri-interfaces/IPv4/Converter.php new file mode 100644 index 000000000..71c0bb9fc --- /dev/null +++ b/vendor/league/uri-interfaces/IPv4/Converter.php @@ -0,0 +1,303 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\IPv4; + +use League\Uri\Exceptions\MissingFeature; +use League\Uri\FeatureDetection; +use Stringable; + +use function array_pop; +use function count; +use function explode; +use function extension_loaded; +use function ltrim; +use function preg_match; +use function str_ends_with; +use function substr; + +use const FILTER_FLAG_IPV4; +use const FILTER_FLAG_IPV6; +use const FILTER_VALIDATE_IP; + +final class Converter +{ + private const REGEXP_IPV4_HOST = '/ + (?(DEFINE) # . is missing as it is used to separate labels + (?<hexadecimal>0x[[:xdigit:]]*) + (?<octal>0[0-7]*) + (?<decimal>\d+) + (?<ipv4_part>(?:(?&hexadecimal)|(?&octal)|(?&decimal))*) + ) + ^(?:(?&ipv4_part)\.){0,3}(?&ipv4_part)\.?$ + /x'; + private const REGEXP_IPV4_NUMBER_PER_BASE = [ + '/^0x(?<number>[[:xdigit:]]*)$/' => 16, + '/^0(?<number>[0-7]*)$/' => 8, + '/^(?<number>\d+)$/' => 10, + ]; + + private const IPV6_6TO4_PREFIX = '2002:'; + private const IPV4_MAPPED_PREFIX = '::ffff:'; + + private readonly mixed $maxIPv4Number; + + public function __construct( + private readonly Calculator $calculator + ) { + $this->maxIPv4Number = $calculator->sub($calculator->pow(2, 32), 1); + } + + /** + * Returns an instance using a GMP calculator. + */ + public static function fromGMP(): self + { + return new self(new GMPCalculator()); + } + + /** + * Returns an instance using a Bcmath calculator. + */ + public static function fromBCMath(): self + { + return new self(new BCMathCalculator()); + } + + /** + * Returns an instance using a PHP native calculator (requires 64bits PHP). + */ + public static function fromNative(): self + { + return new self(new NativeCalculator()); + } + + /** + * Returns an instance using a detected calculator depending on the PHP environment. + * + * @throws MissingFeature If no Calculator implementing object can be used on the platform + * + * @codeCoverageIgnore + */ + public static function fromEnvironment(): self + { + FeatureDetection::supportsIPv4Conversion(); + + return match (true) { + extension_loaded('gmp') => self::fromGMP(), + extension_loaded('bcmath') => self::fromBCMath(), + default => self::fromNative(), + }; + } + + public function isIpv4(Stringable|string|null $host): bool + { + if (null === $host) { + return false; + } + + if (null !== $this->toDecimal($host)) { + return true; + } + + $host = (string) $host; + if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return false; + } + + $ipAddress = strtolower((string) inet_ntop((string) inet_pton($host))); + if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) { + return false !== filter_var(substr($ipAddress, 7), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + + if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) { + return false; + } + + $hexParts = explode(':', substr($ipAddress, 5, 9)); + + return count($hexParts) > 1 + && false !== long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1])); + } + + public function toIPv6Using6to4(Stringable|string|null $host): ?string + { + $host = $this->toDecimal($host); + if (null === $host) { + return null; + } + + /** @var array<string> $parts */ + $parts = array_map( + fn (string $part): string => sprintf('%02x', $part), + explode('.', $host) + ); + + return '['.self::IPV6_6TO4_PREFIX.$parts[0].$parts[1].':'.$parts[2].$parts[3].'::]'; + } + + public function toIPv6UsingMapping(Stringable|string|null $host): ?string + { + $host = $this->toDecimal($host); + if (null === $host) { + return null; + } + + return '['.self::IPV4_MAPPED_PREFIX.$host.']'; + } + + public function toOctal(Stringable|string|null $host): ?string + { + $host = $this->toDecimal($host); + + return match (null) { + $host => null, + default => implode('.', array_map( + fn ($value) => str_pad(decoct((int) $value), 4, '0', STR_PAD_LEFT), + explode('.', $host) + )), + }; + } + + public function toHexadecimal(Stringable|string|null $host): ?string + { + $host = $this->toDecimal($host); + + return match (null) { + $host => null, + default => '0x'.implode('', array_map( + fn ($value) => dechex((int) $value), + explode('.', $host) + )), + }; + } + + /** + * Tries to convert a IPv4 hexadecimal or a IPv4 octal notation into a IPv4 dot-decimal notation if possible + * otherwise returns null. + * + * @see https://url.spec.whatwg.org/#concept-ipv4-parser + */ + public function toDecimal(Stringable|string|null $host): ?string + { + $host = (string) $host; + if (str_starts_with($host, '[') && str_ends_with($host, ']')) { + $host = substr($host, 1, -1); + if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return null; + } + + $ipAddress = strtolower((string) inet_ntop((string) inet_pton($host))); + if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) { + return substr($ipAddress, 7); + } + + if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) { + return null; + } + + $hexParts = explode(':', substr($ipAddress, 5, 9)); + + return (string) match (true) { + count($hexParts) < 2 => null, + default => long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1])), + }; + } + + if (1 !== preg_match(self::REGEXP_IPV4_HOST, $host)) { + return null; + } + + if (str_ends_with($host, '.')) { + $host = substr($host, 0, -1); + } + + $numbers = []; + foreach (explode('.', $host) as $label) { + $number = $this->labelToNumber($label); + if (null === $number) { + return null; + } + + $numbers[] = $number; + } + + $ipv4 = array_pop($numbers); + $max = $this->calculator->pow(256, 6 - count($numbers)); + if ($this->calculator->compare($ipv4, $max) > 0) { + return null; + } + + foreach ($numbers as $offset => $number) { + if ($this->calculator->compare($number, 255) > 0) { + return null; + } + + $ipv4 = $this->calculator->add($ipv4, $this->calculator->multiply( + $number, + $this->calculator->pow(256, 3 - $offset) + )); + } + + return $this->long2Ip($ipv4); + } + + /** + * Converts a domain label into a IPv4 integer part. + * + * @see https://url.spec.whatwg.org/#ipv4-number-parser + * + * @return mixed returns null if it cannot correctly convert the label + */ + private function labelToNumber(string $label): mixed + { + foreach (self::REGEXP_IPV4_NUMBER_PER_BASE as $regexp => $base) { + if (1 !== preg_match($regexp, $label, $matches)) { + continue; + } + + $number = ltrim($matches['number'], '0'); + if ('' === $number) { + return 0; + } + + $number = $this->calculator->baseConvert($number, $base); + if (0 <= $this->calculator->compare($number, 0) && 0 >= $this->calculator->compare($number, $this->maxIPv4Number)) { + return $number; + } + } + + return null; + } + + /** + * Generates the dot-decimal notation for IPv4. + * + * @see https://url.spec.whatwg.org/#concept-ipv4-parser + * + * @param mixed $ipAddress the number representation of the IPV4address + */ + private function long2Ip(mixed $ipAddress): string + { + $output = ''; + for ($offset = 0; $offset < 4; $offset++) { + $output = $this->calculator->mod($ipAddress, 256).$output; + if ($offset < 3) { + $output = '.'.$output; + } + $ipAddress = $this->calculator->div($ipAddress, 256); + } + + return $output; + } +} diff --git a/vendor/league/uri-interfaces/IPv4/GMPCalculator.php b/vendor/league/uri-interfaces/IPv4/GMPCalculator.php new file mode 100644 index 000000000..34db5dfc4 --- /dev/null +++ b/vendor/league/uri-interfaces/IPv4/GMPCalculator.php @@ -0,0 +1,70 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\IPv4; + +use GMP; + +use function gmp_add; +use function gmp_cmp; +use function gmp_div_q; +use function gmp_init; +use function gmp_mod; +use function gmp_mul; +use function gmp_pow; +use function gmp_sub; + +use const GMP_ROUND_MINUSINF; + +final class GMPCalculator implements Calculator +{ + public function baseConvert(mixed $value, int $base): GMP + { + return gmp_init($value, $base); + } + + public function pow(mixed $value, int $exponent): GMP + { + return gmp_pow($value, $exponent); + } + + public function compare(mixed $value1, mixed $value2): int + { + return gmp_cmp($value1, $value2); + } + + public function multiply(mixed $value1, mixed $value2): GMP + { + return gmp_mul($value1, $value2); + } + + public function div(mixed $value, mixed $base): GMP + { + return gmp_div_q($value, $base, GMP_ROUND_MINUSINF); + } + + public function mod(mixed $value, mixed $base): GMP + { + return gmp_mod($value, $base); + } + + public function add(mixed $value1, mixed $value2): GMP + { + return gmp_add($value1, $value2); + } + + public function sub(mixed $value1, mixed $value2): GMP + { + return gmp_sub($value1, $value2); + } +} diff --git a/vendor/league/uri-interfaces/IPv4/NativeCalculator.php b/vendor/league/uri-interfaces/IPv4/NativeCalculator.php new file mode 100644 index 000000000..7ac2c7671 --- /dev/null +++ b/vendor/league/uri-interfaces/IPv4/NativeCalculator.php @@ -0,0 +1,60 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\IPv4; + +use function floor; +use function intval; + +final class NativeCalculator implements Calculator +{ + public function baseConvert(mixed $value, int $base): int + { + return intval((string) $value, $base); + } + + public function pow(mixed $value, int $exponent) + { + return $value ** $exponent; + } + + public function compare(mixed $value1, mixed $value2): int + { + return $value1 <=> $value2; + } + + public function multiply(mixed $value1, mixed $value2): int + { + return $value1 * $value2; + } + + public function div(mixed $value, mixed $base): int + { + return (int) floor($value / $base); + } + + public function mod(mixed $value, mixed $base): int + { + return $value % $base; + } + + public function add(mixed $value1, mixed $value2): int + { + return $value1 + $value2; + } + + public function sub(mixed $value1, mixed $value2): int + { + return $value1 - $value2; + } +} diff --git a/vendor/league/uri-interfaces/IPv6/Converter.php b/vendor/league/uri-interfaces/IPv6/Converter.php new file mode 100644 index 000000000..f645c1da2 --- /dev/null +++ b/vendor/league/uri-interfaces/IPv6/Converter.php @@ -0,0 +1,137 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\IPv6; + +use Stringable; +use ValueError; + +use function filter_var; +use function implode; +use function inet_pton; +use function str_split; +use function strtolower; +use function unpack; + +use const FILTER_FLAG_IPV6; +use const FILTER_VALIDATE_IP; + +final class Converter +{ + /** + * Significant 10 bits of IP to detect Zone ID regular expression pattern. + * + * @var string + */ + private const HOST_ADDRESS_BLOCK = "\xfe\x80"; + + public static function compressIp(string $ipAddress): string + { + return match (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + false => throw new ValueError('The submitted IP is not a valid IPv6 address.'), + default => strtolower((string) inet_ntop((string) inet_pton($ipAddress))), + }; + } + + public static function expandIp(string $ipAddress): string + { + if (false === filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + throw new ValueError('The submitted IP is not a valid IPv6 address.'); + } + + $hex = (array) unpack('H*hex', (string) inet_pton($ipAddress)); + + return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4)); + } + + public static function compress(Stringable|string|null $host): ?string + { + $components = self::parse($host); + if (null === $components['ipAddress']) { + return match ($host) { + null => $host, + default => (string) $host, + }; + } + + $components['ipAddress'] = self::compressIp($components['ipAddress']); + + return self::build($components); + } + + public static function expand(Stringable|string|null $host): ?string + { + $components = self::parse($host); + if (null === $components['ipAddress']) { + return match ($host) { + null => $host, + default => (string) $host, + }; + } + + $components['ipAddress'] = self::expandIp($components['ipAddress']); + + return self::build($components); + } + + private static function build(array $components): string + { + $components['ipAddress'] ??= null; + $components['zoneIdentifier'] ??= null; + + if (null === $components['ipAddress']) { + return ''; + } + + return '['.$components['ipAddress'].match ($components['zoneIdentifier']) { + null => '', + default => '%'.$components['zoneIdentifier'], + }.']'; + } + + /**] + * @param Stringable|string|null $host + * + * @return array{ipAddress:string|null, zoneIdentifier:string|null} + */ + private static function parse(Stringable|string|null $host): array + { + if (null === $host) { + return ['ipAddress' => null, 'zoneIdentifier' => null]; + } + + $host = (string) $host; + if ('' === $host) { + return ['ipAddress' => null, 'zoneIdentifier' => null]; + } + + if (!str_starts_with($host, '[')) { + return ['ipAddress' => null, 'zoneIdentifier' => null]; + } + + if (!str_ends_with($host, ']')) { + return ['ipAddress' => null, 'zoneIdentifier' => null]; + } + + [$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null]; + if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return ['ipAddress' => null, 'zoneIdentifier' => null]; + } + + return match (true) { + null === $zoneIdentifier, + is_string($ipv6) && str_starts_with((string)inet_pton($ipv6), self::HOST_ADDRESS_BLOCK) => ['ipAddress' => $ipv6, 'zoneIdentifier' => $zoneIdentifier], + default => ['ipAddress' => null, 'zoneIdentifier' => null], + }; + } +} diff --git a/vendor/league/uri-interfaces/Idna/Converter.php b/vendor/league/uri-interfaces/Idna/Converter.php new file mode 100644 index 000000000..b993e9e09 --- /dev/null +++ b/vendor/league/uri-interfaces/Idna/Converter.php @@ -0,0 +1,218 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Idna; + +use League\Uri\Exceptions\ConversionFailed; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\FeatureDetection; +use Stringable; + +use function idn_to_ascii; +use function idn_to_utf8; +use function rawurldecode; + +use const INTL_IDNA_VARIANT_UTS46; + +/** + * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html + */ +final class Converter +{ + private const REGEXP_IDNA_PATTERN = '/[^\x20-\x7f]/'; + private const MAX_DOMAIN_LENGTH = 253; + private const MAX_LABEL_LENGTH = 63; + + /** + * General registered name regular expression. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2.2 + * @see https://regex101.com/r/fptU8V/1 + */ + private const REGEXP_REGISTERED_NAME = '/ + (?(DEFINE) + (?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels + (?<sub_delims>[!$&\'()*+,;=]) + (?<encoded>%[A-F0-9]{2}) + (?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*) + ) + ^(?:(?®_name)\.)*(?®_name)\.?$ + /ix'; + + /** + * Converts the input to its IDNA ASCII form or throw on failure. + * + * @see Converter::toAscii() + * + * @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm + * @throws ConversionFailed if the conversion returns error + */ + public static function toAsciiOrFail(Stringable|string $domain, Option|int|null $options = null): string + { + $result = self::toAscii($domain, $options); + + return match (true) { + $result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result), + default => $result->domain(), + }; + } + + /** + * Converts the input to its IDNA ASCII form. + * + * This method returns the string converted to IDN ASCII form + * + * @throws SyntaxError if the string cannot be converted to ASCII using IDN UTS46 algorithm + */ + public static function toAscii(Stringable|string $domain, Option|int|null $options = null): Result + { + $domain = rawurldecode((string) $domain); + + if (1 === preg_match(self::REGEXP_IDNA_PATTERN, $domain)) { + FeatureDetection::supportsIdn(); + + $flags = match (true) { + null === $options => Option::forIDNA2008Ascii(), + $options instanceof Option => $options, + default => Option::new($options), + }; + + idn_to_ascii($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo); + + if ([] === $idnaInfo) { + return Result::fromIntl([ + 'result' => strtolower($domain), + 'isTransitionalDifferent' => false, + 'errors' => self::validateDomainAndLabelLength($domain), + ]); + } + + return Result::fromIntl($idnaInfo); + } + + $error = Error::NONE->value; + if (1 !== preg_match(self::REGEXP_REGISTERED_NAME, $domain)) { + $error |= Error::DISALLOWED->value; + } + + return Result::fromIntl([ + 'result' => strtolower($domain), + 'isTransitionalDifferent' => false, + 'errors' => self::validateDomainAndLabelLength($domain) | $error, + ]); + } + + /** + * Converts the input to its IDNA UNICODE form or throw on failure. + * + * @see Converter::toUnicode() + * + * @throws ConversionFailed if the conversion returns error + */ + public static function toUnicodeOrFail(Stringable|string $domain, Option|int|null $options = null): string + { + $result = self::toUnicode($domain, $options); + + return match (true) { + $result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result), + default => $result->domain(), + }; + } + + /** + * Converts the input to its IDNA UNICODE form. + * + * This method returns the string converted to IDN UNICODE form + * + * @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm + */ + public static function toUnicode(Stringable|string $domain, Option|int|null $options = null): Result + { + $domain = rawurldecode((string) $domain); + + if (false === stripos($domain, 'xn--')) { + return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]); + } + + FeatureDetection::supportsIdn(); + + $flags = match (true) { + null === $options => Option::forIDNA2008Unicode(), + $options instanceof Option => $options, + default => Option::new($options), + }; + + idn_to_utf8($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo); + + if ([] === $idnaInfo) { + return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]); + } + + return Result::fromIntl($idnaInfo); + } + + /** + * Tells whether the submitted host is a valid IDN regardless of its format. + * + * Returns false if the host is invalid or if its conversion yield the same result + */ + public static function isIdn(Stringable|string|null $domain): bool + { + $domain = strtolower(rawurldecode((string) $domain)); + $result = match (1) { + preg_match(self::REGEXP_IDNA_PATTERN, $domain) => self::toAscii($domain), + default => self::toUnicode($domain), + }; + + return match (true) { + $result->hasErrors() => false, + default => $result->domain() !== $domain, + }; + } + + /** + * Adapted from https://github.com/TRowbotham/idna. + * + * @see https://github.com/TRowbotham/idna/blob/master/src/Idna.php#L236 + */ + private static function validateDomainAndLabelLength(string $domain): int + { + $error = Error::NONE->value; + $labels = explode('.', $domain); + $maxDomainSize = self::MAX_DOMAIN_LENGTH; + $length = count($labels); + + // If the last label is empty, and it is not the first label, then it is the root label. + // Increase the max size by 1, making it 254, to account for the root label's "." + // delimiter. This also means we don't need to check the last label's length for being too + // long. + if ($length > 1 && '' === $labels[$length - 1]) { + ++$maxDomainSize; + array_pop($labels); + } + + if (strlen($domain) > $maxDomainSize) { + $error |= Error::DOMAIN_NAME_TOO_LONG->value; + } + + foreach ($labels as $label) { + if (strlen($label) > self::MAX_LABEL_LENGTH) { + $error |= Error::LABEL_TOO_LONG->value; + + break; + } + } + + return $error; + } +} diff --git a/vendor/league/uri-interfaces/Idna/Error.php b/vendor/league/uri-interfaces/Idna/Error.php new file mode 100644 index 000000000..eb51e9dd5 --- /dev/null +++ b/vendor/league/uri-interfaces/Idna/Error.php @@ -0,0 +1,64 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace League\Uri\Idna; + +enum Error: int +{ + case NONE = 0; + case EMPTY_LABEL = 1; + case LABEL_TOO_LONG = 2; + case DOMAIN_NAME_TOO_LONG = 4; + case LEADING_HYPHEN = 8; + case TRAILING_HYPHEN = 0x10; + case HYPHEN_3_4 = 0x20; + case LEADING_COMBINING_MARK = 0x40; + case DISALLOWED = 0x80; + case PUNYCODE = 0x100; + case LABEL_HAS_DOT = 0x200; + case INVALID_ACE_LABEL = 0x400; + case BIDI = 0x800; + case CONTEXTJ = 0x1000; + case CONTEXTO_PUNCTUATION = 0x2000; + case CONTEXTO_DIGITS = 0x4000; + + public function description(): string + { + return match ($this) { + self::NONE => 'No error has occurred', + self::EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty', + self::LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes', + self::DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form', + self::LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")', + self::TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")', + self::HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions', + self::LEADING_COMBINING_MARK => 'a label starts with a combining mark', + self::DISALLOWED => 'a label or domain name contains disallowed characters', + self::PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode', + self::LABEL_HAS_DOT => 'a label contains a dot=full stop', + self::INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string', + self::BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)', + self::CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements', + self::CONTEXTO_DIGITS => 'a label does not meet the IDNA CONTEXTO requirements for digits', + self::CONTEXTO_PUNCTUATION => 'a label does not meet the IDNA CONTEXTO requirements for punctuation characters. Some punctuation characters "Would otherwise have been DISALLOWED" but are allowed in certain contexts', + }; + } + + public static function filterByErrorBytes(int $errors): array + { + return array_values( + array_filter( + self::cases(), + fn (self $error): bool => 0 !== ($error->value & $errors) + ) + ); + } +} diff --git a/vendor/league/uri-interfaces/Idna/Option.php b/vendor/league/uri-interfaces/Idna/Option.php new file mode 100644 index 000000000..777979f13 --- /dev/null +++ b/vendor/league/uri-interfaces/Idna/Option.php @@ -0,0 +1,179 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Idna; + +use ReflectionClass; +use ReflectionClassConstant; + +/** + * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html + */ +final class Option +{ + private const DEFAULT = 0; + private const ALLOW_UNASSIGNED = 1; + private const USE_STD3_RULES = 2; + private const CHECK_BIDI = 4; + private const CHECK_CONTEXTJ = 8; + private const NONTRANSITIONAL_TO_ASCII = 0x10; + private const NONTRANSITIONAL_TO_UNICODE = 0x20; + private const CHECK_CONTEXTO = 0x40; + + private function __construct(private readonly int $value) + { + } + + private static function cases(): array + { + static $assoc; + if (null === $assoc) { + $assoc = []; + $fooClass = new ReflectionClass(self::class); + foreach ($fooClass->getConstants(ReflectionClassConstant::IS_PRIVATE) as $name => $value) { + $assoc[$name] = $value; + } + } + + return $assoc; + } + + public static function new(int $bytes = self::DEFAULT): self + { + return new self(array_reduce( + self::cases(), + fn (int $value, int $option) => 0 !== ($option & $bytes) ? ($value | $option) : $value, + self::DEFAULT + )); + } + + public static function forIDNA2008Ascii(): self + { + return self::new() + ->nonTransitionalToAscii() + ->checkBidi() + ->useSTD3Rules() + ->checkContextJ(); + } + + public static function forIDNA2008Unicode(): self + { + return self::new() + ->nonTransitionalToUnicode() + ->checkBidi() + ->useSTD3Rules() + ->checkContextJ(); + } + + public function toBytes(): int + { + return $this->value; + } + + /** array<string, int> */ + public function list(): array + { + return array_keys(array_filter( + self::cases(), + fn (int $value) => 0 !== ($value & $this->value) + )); + } + + public function allowUnassigned(): self + { + return $this->add(self::ALLOW_UNASSIGNED); + } + + public function disallowUnassigned(): self + { + return $this->remove(self::ALLOW_UNASSIGNED); + } + + public function useSTD3Rules(): self + { + return $this->add(self::USE_STD3_RULES); + } + + public function prohibitSTD3Rules(): self + { + return $this->remove(self::USE_STD3_RULES); + } + + public function checkBidi(): self + { + return $this->add(self::CHECK_BIDI); + } + + public function ignoreBidi(): self + { + return $this->remove(self::CHECK_BIDI); + } + + public function checkContextJ(): self + { + return $this->add(self::CHECK_CONTEXTJ); + } + + public function ignoreContextJ(): self + { + return $this->remove(self::CHECK_CONTEXTJ); + } + + public function checkContextO(): self + { + return $this->add(self::CHECK_CONTEXTO); + } + + public function ignoreContextO(): self + { + return $this->remove(self::CHECK_CONTEXTO); + } + + public function nonTransitionalToAscii(): self + { + return $this->add(self::NONTRANSITIONAL_TO_ASCII); + } + + public function transitionalToAscii(): self + { + return $this->remove(self::NONTRANSITIONAL_TO_ASCII); + } + + public function nonTransitionalToUnicode(): self + { + return $this->add(self::NONTRANSITIONAL_TO_UNICODE); + } + + public function transitionalToUnicode(): self + { + return $this->remove(self::NONTRANSITIONAL_TO_UNICODE); + } + + public function add(Option|int|null $option = null): self + { + return match (true) { + null === $option => $this, + $option instanceof self => self::new($this->value | $option->value), + default => self::new($this->value | $option), + }; + } + + public function remove(Option|int|null $option = null): self + { + return match (true) { + null === $option => $this, + $option instanceof self => self::new($this->value & ~$option->value), + default => self::new($this->value & ~$option), + }; + } +} diff --git a/vendor/league/uri-interfaces/Idna/Result.php b/vendor/league/uri-interfaces/Idna/Result.php new file mode 100644 index 000000000..02d713f5c --- /dev/null +++ b/vendor/league/uri-interfaces/Idna/Result.php @@ -0,0 +1,64 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Idna; + +/** + * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html + */ +final class Result +{ + private function __construct( + private readonly string $domain, + private readonly bool $isTransitionalDifferent, + /** @var array<Error> */ + private readonly array $errors + ) { + } + + /** + * @param array{result:string, isTransitionalDifferent:bool, errors:int} $infos + */ + public static function fromIntl(array $infos): self + { + return new self($infos['result'], $infos['isTransitionalDifferent'], Error::filterByErrorBytes($infos['errors'])); + } + + public function domain(): string + { + return $this->domain; + } + + public function isTransitionalDifferent(): bool + { + return $this->isTransitionalDifferent; + } + + /** + * @return array<Error> + */ + public function errors(): array + { + return $this->errors; + } + + public function hasErrors(): bool + { + return [] !== $this->errors; + } + + public function hasError(Error $error): bool + { + return in_array($error, $this->errors, true); + } +} diff --git a/vendor/league/uri-interfaces/KeyValuePair/Converter.php b/vendor/league/uri-interfaces/KeyValuePair/Converter.php new file mode 100644 index 000000000..f208060c6 --- /dev/null +++ b/vendor/league/uri-interfaces/KeyValuePair/Converter.php @@ -0,0 +1,209 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\KeyValuePair; + +use League\Uri\Contracts\UriComponentInterface; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +use function array_combine; +use function explode; +use function implode; +use function is_float; +use function is_int; +use function is_string; +use function json_encode; +use function preg_match; +use function str_replace; + +use const JSON_PRESERVE_ZERO_FRACTION; +use const PHP_QUERY_RFC1738; +use const PHP_QUERY_RFC3986; + +final class Converter +{ + private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/'; + + /** + * @param non-empty-string $separator the query string separator + * @param array<string> $fromRfc3986 contains all the RFC3986 encoded characters to be converted + * @param array<string> $toEncoding contains all the expected encoded characters + */ + private function __construct( + private readonly string $separator, + private readonly array $fromRfc3986 = [], + private readonly array $toEncoding = [], + ) { + if ('' === $this->separator) { + throw new SyntaxError('The separator character must be a non empty string.'); + } + } + + /** + * @param non-empty-string $separator + */ + public static function new(string $separator): self + { + return new self($separator); + } + + /** + * @param non-empty-string $separator + */ + public static function fromRFC3986(string $separator = '&'): self + { + return self::new($separator); + } + + /** + * @param non-empty-string $separator + */ + public static function fromRFC1738(string $separator = '&'): self + { + return self::new($separator) + ->withEncodingMap(['%20' => '+']); + } + + /** + * @param non-empty-string $separator + * + * @see https://url.spec.whatwg.org/#application/x-www-form-urlencoded + */ + public static function fromFormData(string $separator = '&'): self + { + return self::new($separator) + ->withEncodingMap(['%20' => '+', '%2A' => '*']); + } + + public static function fromEncodingType(int $encType): self + { + return match ($encType) { + PHP_QUERY_RFC3986 => self::fromRFC3986(), + PHP_QUERY_RFC1738 => self::fromRFC1738(), + default => throw new SyntaxError('Unknown or Unsupported encoding.'), + }; + } + + /** + * @return non-empty-string + */ + public function separator(): string + { + return $this->separator; + } + + /** + * @return array<string, string> + */ + public function encodingMap(): array + { + return array_combine($this->fromRfc3986, $this->toEncoding); + } + + /** + * @return array<non-empty-list<string|null>> + */ + public function toPairs(Stringable|string|int|float|bool|null $value): array + { + $value = match (true) { + $value instanceof UriComponentInterface => $value->value(), + $value instanceof Stringable, is_int($value) => (string) $value, + false === $value => '0', + true === $value => '1', + default => $value, + }; + + if (null === $value) { + return []; + } + + $value = match (1) { + preg_match(self::REGEXP_INVALID_CHARS, (string) $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'), + default => str_replace($this->toEncoding, $this->fromRfc3986, (string) $value), + }; + + return array_map( + fn (string $pair): array => explode('=', $pair, 2) + [1 => null], + explode($this->separator, $value) + ); + } + + private static function vString(Stringable|string|bool|int|float|null $value): ?string + { + return match (true) { + $value => '1', + false === $value => '0', + null === $value => null, + is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION), + default => (string) $value, + }; + } + + /** + * @param iterable<array{0:string|null, 1:Stringable|string|bool|int|float|null}> $pairs + */ + public function toValue(iterable $pairs): ?string + { + $filteredPairs = []; + foreach ($pairs as $pair) { + $filteredPairs[] = match (true) { + !is_string($pair[0]) => throw new SyntaxError('the pair key MUST be a string;, `'.gettype($pair[0]).'` given.'), + null === $pair[1] => self::vString($pair[0]), + default => self::vString($pair[0]).'='.self::vString($pair[1]), + }; + } + + return match ([]) { + $filteredPairs => null, + default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)), + }; + } + + /** + * @param non-empty-string $separator + */ + public function withSeparator(string $separator): self + { + return match ($this->separator) { + $separator => $this, + default => new self($separator, $this->fromRfc3986, $this->toEncoding), + }; + } + + /** + * Sets the conversion map. + * + * Each key from the iterable structure represents the RFC3986 encoded characters as string, + * while each value represents the expected output encoded characters + */ + public function withEncodingMap(iterable $encodingMap): self + { + $fromRfc3986 = []; + $toEncoding = []; + foreach ($encodingMap as $from => $to) { + [$fromRfc3986[], $toEncoding[]] = match (true) { + !is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'), + $to instanceof Stringable, + is_string($to) => [$from, (string) $to], + default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'), + }; + } + + return match (true) { + $fromRfc3986 !== $this->fromRfc3986, + $toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding), + default => $this, + }; + } +} diff --git a/vendor/league/uri-interfaces/LICENSE b/vendor/league/uri-interfaces/LICENSE new file mode 100644 index 000000000..3b52528f2 --- /dev/null +++ b/vendor/league/uri-interfaces/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 ignace nyamagana butera + +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. diff --git a/vendor/league/uri-interfaces/QueryString.php b/vendor/league/uri-interfaces/QueryString.php new file mode 100644 index 000000000..b35086736 --- /dev/null +++ b/vendor/league/uri-interfaces/QueryString.php @@ -0,0 +1,276 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use League\Uri\Exceptions\SyntaxError; +use League\Uri\KeyValuePair\Converter; +use Stringable; + +use function array_key_exists; +use function array_keys; +use function is_array; +use function rawurldecode; +use function strpos; +use function substr; + +use const PHP_QUERY_RFC3986; + +/** + * A class to parse the URI query string. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + */ +final class QueryString +{ + private const PAIR_VALUE_DECODED = 1; + private const PAIR_VALUE_PRESERVED = 2; + + /** + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Build a query string from a list of pairs. + * + * @see QueryString::buildFromPairs() + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + * + * @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs + * @param non-empty-string $separator + * + * @throws SyntaxError If the encoding type is invalid + * @throws SyntaxError If a pair is invalid + */ + public static function build(iterable $pairs, string $separator = '&', int $encType = PHP_QUERY_RFC3986): ?string + { + return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator)); + } + + /** + * Build a query string from a list of pairs. + * + * The method expects the return value from Query::parse to build + * a valid query string. This method differs from PHP http_build_query as + * it does not modify parameters keys. + * + * If a reserved character is found in a URI component and + * no delimiting role is known for that character, then it must be + * interpreted as representing the data octet corresponding to that + * character's encoding in US-ASCII. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + * + * @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs + * + * @throws SyntaxError If the encoding type is invalid + * @throws SyntaxError If a pair is invalid + */ + public static function buildFromPairs(iterable $pairs, ?Converter $converter = null): ?string + { + $keyValuePairs = []; + foreach ($pairs as $pair) { + if (!is_array($pair) || [0, 1] !== array_keys($pair)) { + throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.'); + } + + $keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($pair[0]), match(null) { + $pair[1] => null, + default => Encoder::encodeQueryKeyValue($pair[1]), + }]; + } + + return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs); + } + + /** + * Parses the query string like parse_str without mangling the results. + * + * @see QueryString::extractFromValue() + * @see http://php.net/parse_str + * @see https://wiki.php.net/rfc/on_demand_name_mangling + * + * @param non-empty-string $separator + * + * @throws SyntaxError + */ + public static function extract(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array + { + return self::extractFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator)); + } + + /** + * Parses the query string like parse_str without mangling the results. + * + * The result is similar as PHP parse_str when used with its + * second argument with the difference that variable names are + * not mangled. + * + * @see http://php.net/parse_str + * @see https://wiki.php.net/rfc/on_demand_name_mangling + * + * @throws SyntaxError + */ + public static function extractFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array + { + return self::convert(self::decodePairs( + ($converter ?? Converter::fromRFC3986())->toPairs($query), + self::PAIR_VALUE_PRESERVED + )); + } + + /** + * Parses a query string into a collection of key/value pairs. + * + * @param non-empty-string $separator + * + * @throws SyntaxError + * + * @return array<int, array{0:string, 1:string|null}> + */ + public static function parse(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array + { + return self::parseFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator)); + } + + /** + * Parses a query string into a collection of key/value pairs. + * + * @throws SyntaxError + * + * @return array<int, array{0:string, 1:string|null}> + */ + public static function parseFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array + { + return self::decodePairs( + ($converter ?? Converter::fromRFC3986())->toPairs($query), + self::PAIR_VALUE_DECODED + ); + } + + /** + * @param array<non-empty-list<string|null>> $pairs + * + * @return array<int, array{0:string, 1:string|null}> + */ + private static function decodePairs(array $pairs, int $pairValueState): array + { + $decodePair = static function (array $pair, int $pairValueState): array { + [$key, $value] = $pair; + + return match ($pairValueState) { + self::PAIR_VALUE_PRESERVED => [(string) Encoder::decodeAll($key), $value], + default => [(string) Encoder::decodeAll($key), Encoder::decodeAll($value)], + }; + }; + + return array_reduce( + $pairs, + fn (array $carry, array $pair) => [...$carry, $decodePair($pair, $pairValueState)], + [] + ); + } + + /** + * Converts a collection of key/value pairs and returns + * the store PHP variables as elements of an array. + */ + public static function convert(iterable $pairs): array + { + $returnedValue = []; + foreach ($pairs as $pair) { + $returnedValue = self::extractPhpVariable($returnedValue, $pair); + } + + return $returnedValue; + } + + /** + * Parses a query pair like parse_str without mangling the results array keys. + * + * <ul> + * <li>empty name are not saved</li> + * <li>If the value from name is duplicated its corresponding value will be overwritten</li> + * <li>if no "[" is detected the value is added to the return array with the name as index</li> + * <li>if no "]" is detected after detecting a "[" the value is added to the return array with the name as index</li> + * <li>if there's a mismatch in bracket usage the remaining part is dropped</li> + * <li>“.” and “ ” are not converted to “_”</li> + * <li>If there is no “]”, then the first “[” is not converted to becomes an “_”</li> + * <li>no whitespace trimming is done on the key value</li> + * </ul> + * + * @see https://php.net/parse_str + * @see https://wiki.php.net/rfc/on_demand_name_mangling + * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic1.phpt + * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic2.phpt + * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic3.phpt + * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic4.phpt + * + * @param array $data the submitted array + * @param array|string $name the pair key + * @param string $value the pair value + */ + private static function extractPhpVariable(array $data, array|string $name, string $value = ''): array + { + if (is_array($name)) { + [$name, $value] = $name; + $value = rawurldecode((string) $value); + } + + if ('' === $name) { + return $data; + } + + $leftBracketPosition = strpos($name, '['); + if (false === $leftBracketPosition) { + $data[$name] = $value; + + return $data; + } + + $rightBracketPosition = strpos($name, ']', $leftBracketPosition); + if (false === $rightBracketPosition) { + $data[$name] = $value; + + return $data; + } + + $key = substr($name, 0, $leftBracketPosition); + if ('' === $key) { + $key = '0'; + } + + if (!array_key_exists($key, $data) || !is_array($data[$key])) { + $data[$key] = []; + } + + $remaining = substr($name, $rightBracketPosition + 1); + if (!str_starts_with($remaining, '[') || !str_contains($remaining, ']')) { + $remaining = ''; + } + + $name = substr($name, $leftBracketPosition + 1, $rightBracketPosition - $leftBracketPosition - 1).$remaining; + if ('' === $name) { + $data[$key][] = $value; + + return $data; + } + + $data[$key] = self::extractPhpVariable($data[$key], $name, $value); + + return $data; + } +} diff --git a/vendor/league/uri-interfaces/UriString.php b/vendor/league/uri-interfaces/UriString.php new file mode 100644 index 000000000..a79184982 --- /dev/null +++ b/vendor/league/uri-interfaces/UriString.php @@ -0,0 +1,513 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use League\Uri\Exceptions\ConversionFailed; +use League\Uri\Exceptions\MissingFeature; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\Idna\Converter; +use Stringable; + +use function array_merge; +use function explode; +use function filter_var; +use function inet_pton; +use function preg_match; +use function rawurldecode; +use function sprintf; +use function strpos; +use function substr; + +use const FILTER_FLAG_IPV6; +use const FILTER_VALIDATE_IP; + +/** + * A class to parse a URI string according to RFC3986. + * + * @link https://tools.ietf.org/html/rfc3986 + * @package League\Uri + * @author Ignace Nyamagana Butera <nyamsprod@gmail.com> + * @since 6.0.0 + * + * @phpstan-type AuthorityMap array{user:?string, pass:?string, host:?string, port:?int} + * @phpstan-type ComponentMap array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} + * @phpstan-type InputComponentMap array{scheme? : ?string, user? : ?string, pass? : ?string, host? : ?string, port? : ?int, path? : ?string, query? : ?string, fragment? : ?string} + */ +final class UriString +{ + /** + * Default URI component values. + * + * @var ComponentMap + */ + private const URI_COMPONENTS = [ + 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, + 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, + ]; + + /** + * Simple URI which do not need any parsing. + * + * @var array<string, array<string>> + */ + private const URI_SHORTCUTS = [ + '' => [], + '#' => ['fragment' => ''], + '?' => ['query' => ''], + '?#' => ['query' => '', 'fragment' => ''], + '/' => ['path' => '/'], + '//' => ['host' => ''], + ]; + + /** + * Range of invalid characters in URI string. + * + * @var string + */ + private const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/'; + + /** + * RFC3986 regular expression URI splitter. + * + * @link https://tools.ietf.org/html/rfc3986#appendix-B + * @var string + */ + private const REGEXP_URI_PARTS = ',^ + (?<scheme>(?<scontent>[^:/?\#]+):)? # URI scheme component + (?<authority>//(?<acontent>[^/?\#]*))? # URI authority part + (?<path>[^?\#]*) # URI path component + (?<query>\?(?<qcontent>[^\#]*))? # URI query component + (?<fragment>\#(?<fcontent>.*))? # URI fragment component + ,x'; + + /** + * URI scheme regular expression. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.1 + * @var string + */ + private const REGEXP_URI_SCHEME = '/^([a-z][a-z\d+.-]*)?$/i'; + + /** + * IPvFuture regular expression. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * @var string + */ + private const REGEXP_IP_FUTURE = '/^ + v(?<version>[A-F0-9])+\. + (?: + (?<unreserved>[a-z0-9_~\-\.])| + (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character + )+ + $/ix'; + + /** + * General registered name regular expression. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * @var string + */ + private const REGEXP_REGISTERED_NAME = '/(?(DEFINE) + (?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels + (?<sub_delims>[!$&\'()*+,;=]) + (?<encoded>%[A-F0-9]{2}) + (?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*) + ) + ^(?:(?®_name)\.)*(?®_name)\.?$/ix'; + + /** + * Invalid characters in host regular expression. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * @var string + */ + private const REGEXP_INVALID_HOST_CHARS = '/ + [:\/?#\[\]@ ] # gen-delims characters as well as the space character + /ix'; + + /** + * Invalid path for URI without scheme and authority regular expression. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.3 + * @var string + */ + private const REGEXP_INVALID_PATH = ',^(([^/]*):)(.*)?/,'; + + /** + * Host and Port splitter regular expression. + * + * @var string + */ + private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,'; + + /** + * IDN Host detector regular expression. + * + * @var string + */ + private const REGEXP_IDN_PATTERN = '/[^\x20-\x7f]/'; + + /** + * Only the address block fe80::/10 can have a Zone ID attach to + * let's detect the link local significant 10 bits. + * + * @var string + */ + private const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80"; + + /** + * Maximum number of host cached. + * + * @var int + */ + private const MAXIMUM_HOST_CACHED = 100; + + /** + * Generate a URI string representation from its parsed representation + * returned by League\UriString::parse() or PHP's parse_url. + * + * If you supply your own array, you are responsible for providing + * valid components without their URI delimiters. + * + * @link https://tools.ietf.org/html/rfc3986#section-5.3 + * @link https://tools.ietf.org/html/rfc3986#section-7.5 + * + * @param InputComponentMap $components + */ + public static function build(array $components): string + { + return self::buildUri( + $components['scheme'] ?? null, + self::buildAuthority($components), + $components['path'] ?? '', + $components['query'] ?? null, + $components['fragment'] ?? null, + ); + } + + /** + * Generate a URI string representation based on RFC3986 algorithm. + * + * valid URI component MUST be provided without their URI delimiters + * but properly encoded. + * + * @link https://tools.ietf.org/html/rfc3986#section-5.3 + * @link https://tools.ietf.org/html/rfc3986#section-7.5 + */ + public static function buildUri( + ?string $scheme, + ?string $authority, + string $path, + ?string $query, + ?string $fragment, + ): string { + $uri = ''; + if (null !== $scheme) { + $uri .= $scheme.':'; + } + + if (null !== $authority) { + $uri .= '//'.$authority; + } + + $uri .= $path; + if (null !== $query) { + $uri .= '?'.$query; + } + + if (null !== $fragment) { + $uri .= '#'.$fragment; + } + + return $uri; + } + + /** + * Generate a URI authority representation from its parsed representation. + * + * @param InputComponentMap $components + */ + public static function buildAuthority(array $components): ?string + { + if (!isset($components['host'])) { + return null; + } + + $authority = $components['host']; + if (isset($components['port'])) { + $authority .= ':'.$components['port']; + } + + if (!isset($components['user'])) { + return $authority; + } + + $authority = '@'.$authority; + if (!isset($components['pass'])) { + return $components['user'].$authority; + } + + return $components['user'].':'.$components['pass'].$authority; + } + + /** + * Parse a URI string into its components. + * + * This method parses a URI and returns an associative array containing any + * of the various components of the URI that are present. + * + * <code> + * $components = UriString::parse('http://foo@test.example.com:42?query#'); + * var_export($components); + * //will display + * array( + * 'scheme' => 'http', // the URI scheme component + * 'user' => 'foo', // the URI user component + * 'pass' => null, // the URI pass component + * 'host' => 'test.example.com', // the URI host component + * 'port' => 42, // the URI port component + * 'path' => '', // the URI path component + * 'query' => 'query', // the URI query component + * 'fragment' => '', // the URI fragment component + * ); + * </code> + * + * The returned array is similar to PHP's parse_url return value with the following + * differences: + * + * <ul> + * <li>All components are always present in the returned array</li> + * <li>Empty and undefined component are treated differently. And empty component is + * set to the empty string while an undefined component is set to the `null` value.</li> + * <li>The path component is never undefined</li> + * <li>The method parses the URI following the RFC3986 rules, but you are still + * required to validate the returned components against its related scheme specific rules.</li> + * </ul> + * + * @link https://tools.ietf.org/html/rfc3986 + * + * @throws SyntaxError if the URI contains invalid characters + * @throws SyntaxError if the URI contains an invalid scheme + * @throws SyntaxError if the URI contains an invalid path + * + * @return ComponentMap + */ + public static function parse(Stringable|string|int $uri): array + { + $uri = (string) $uri; + if (isset(self::URI_SHORTCUTS[$uri])) { + /** @var ComponentMap $components */ + $components = array_merge(self::URI_COMPONENTS, self::URI_SHORTCUTS[$uri]); + + return $components; + } + + if (1 === preg_match(self::REGEXP_INVALID_URI_CHARS, $uri)) { + throw new SyntaxError(sprintf('The uri `%s` contains invalid characters', $uri)); + } + + //if the first character is a known URI delimiter parsing can be simplified + $first_char = $uri[0]; + + //The URI is made of the fragment only + if ('#' === $first_char) { + [, $fragment] = explode('#', $uri, 2); + $components = self::URI_COMPONENTS; + $components['fragment'] = $fragment; + + return $components; + } + + //The URI is made of the query and fragment + if ('?' === $first_char) { + [, $partial] = explode('?', $uri, 2); + [$query, $fragment] = explode('#', $partial, 2) + [1 => null]; + $components = self::URI_COMPONENTS; + $components['query'] = $query; + $components['fragment'] = $fragment; + + return $components; + } + + //use RFC3986 URI regexp to split the URI + preg_match(self::REGEXP_URI_PARTS, $uri, $parts); + $parts += ['query' => '', 'fragment' => '']; + + if (':' === ($parts['scheme'] ?? null) || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'] ?? '')) { + throw new SyntaxError(sprintf('The uri `%s` contains an invalid scheme', $uri)); + } + + if ('' === ($parts['scheme'] ?? '').($parts['authority'] ?? '') && 1 === preg_match(self::REGEXP_INVALID_PATH, $parts['path'] ?? '')) { + throw new SyntaxError(sprintf('The uri `%s` contains an invalid path.', $uri)); + } + + /** @var ComponentMap $components */ + $components = array_merge( + self::URI_COMPONENTS, + '' === ($parts['authority'] ?? null) ? [] : self::parseAuthority($parts['acontent'] ?? null), + [ + 'path' => $parts['path'] ?? '', + 'scheme' => '' === ($parts['scheme'] ?? null) ? null : ($parts['scontent'] ?? null), + 'query' => '' === $parts['query'] ? null : ($parts['qcontent'] ?? null), + 'fragment' => '' === $parts['fragment'] ? null : ($parts['fcontent'] ?? null), + ] + ); + + return $components; + } + + /** + * Parses the URI authority part. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2 + * + * @throws SyntaxError If the port component is invalid + * + * @return AuthorityMap + */ + public static function parseAuthority(Stringable|string|null $authority): array + { + $components = ['user' => null, 'pass' => null, 'host' => null, 'port' => null]; + if (null === $authority) { + return $components; + } + + $authority = (string) $authority; + $components['host'] = ''; + if ('' === $authority) { + return $components; + } + + $parts = explode('@', $authority, 2); + if (isset($parts[1])) { + [$components['user'], $components['pass']] = explode(':', $parts[0], 2) + [1 => null]; + } + + preg_match(self::REGEXP_HOST_PORT, $parts[1] ?? $parts[0], $matches); + $matches += ['port' => '']; + + $components['port'] = self::filterPort($matches['port']); + $components['host'] = self::filterHost($matches['host'] ?? ''); + + return $components; + } + + /** + * Filter and format the port component. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @throws SyntaxError if the registered name is invalid + */ + private static function filterPort(string $port): ?int + { + return match (true) { + '' === $port => null, + 1 === preg_match('/^\d*$/', $port) => (int) $port, + default => throw new SyntaxError(sprintf('The port `%s` is invalid', $port)), + }; + } + + /** + * Returns whether a hostname is valid. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @throws SyntaxError if the registered name is invalid + */ + private static function filterHost(string $host): string + { + if ('' === $host) { + return $host; + } + + /** @var array<string, 1> $hostCache */ + static $hostCache = []; + if (isset($hostCache[$host])) { + return $host; + } + + if (self::MAXIMUM_HOST_CACHED < count($hostCache)) { + array_shift($hostCache); + } + + if ('[' !== $host[0] || !str_ends_with($host, ']')) { + self::filterRegisteredName($host); + $hostCache[$host] = 1; + + return $host; + } + + if (self::isIpHost(substr($host, 1, -1))) { + $hostCache[$host] = 1; + + return $host; + } + + throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host)); + } + + /** + * Throws if the host is not a registered name and not a valid IDN host. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @throws SyntaxError if the registered name is invalid + * @throws MissingFeature if IDN support or ICU requirement are not available or met. + * @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form + */ + private static function filterRegisteredName(string $host): void + { + $formattedHost = rawurldecode($host); + if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formattedHost)) { + return; + } + + //to test IDN host non-ascii characters must be present in the host + if (1 !== preg_match(self::REGEXP_IDN_PATTERN, $formattedHost)) { + throw new SyntaxError(sprintf('Host `%s` is invalid: the host is not a valid registered name', $host)); + } + + Converter::toAsciiOrFail($host); + } + + /** + * Validates a IPv6/IPfuture host. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * @link https://tools.ietf.org/html/rfc6874#section-2 + * @link https://tools.ietf.org/html/rfc6874#section-4 + */ + private static function isIpHost(string $ipHost): bool + { + if (false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return true; + } + + if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) { + return !in_array($matches['version'], ['4', '6'], true); + } + + $pos = strpos($ipHost, '%'); + if (false === $pos || 1 === preg_match(self::REGEXP_INVALID_HOST_CHARS, rawurldecode(substr($ipHost, $pos)))) { + return false; + } + + $ipHost = substr($ipHost, 0, $pos); + + return false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) + && str_starts_with((string)inet_pton($ipHost), self::ZONE_ID_ADDRESS_BLOCK); + } +} diff --git a/vendor/league/uri-interfaces/composer.json b/vendor/league/uri-interfaces/composer.json new file mode 100644 index 000000000..1e36a98a2 --- /dev/null +++ b/vendor/league/uri-interfaces/composer.json @@ -0,0 +1,70 @@ +{ + "name": "league/uri-interfaces", + "type": "library", + "description" : "Common interfaces and classes for URI representation and interaction", + "keywords": [ + "url", + "uri", + "rfc3986", + "rfc3987", + "rfc6570", + "psr-7", + "parse_url", + "http", + "https", + "ws", + "ftp", + "data-uri", + "file-uri", + "parse_str", + "query-string", + "querystring", + "hostname" + ], + "license": "MIT", + "homepage": "https://uri.thephpleague.com", + "authors": [ + { + "name" : "Ignace Nyamagana Butera", + "email" : "nyamsprod@gmail.com", + "homepage" : "https://nyamsprod.com" + } + ], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nyamsprod" + } + ], + "require": { + "php" : "^8.1", + "ext-filter": "*", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-factory": "^1" + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "support": { + "forum": "https://thephpleague.slack.com", + "docs": "https://uri.thephpleague.com", + "issues": "https://github.com/thephpleague/uri-src/issues" + }, + "config": { + "sort-packages": true + } +} diff --git a/vendor/league/uri/BaseUri.php b/vendor/league/uri/BaseUri.php new file mode 100644 index 000000000..c29dc2799 --- /dev/null +++ b/vendor/league/uri/BaseUri.php @@ -0,0 +1,658 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use JsonSerializable; +use League\Uri\Contracts\UriAccess; +use League\Uri\Contracts\UriInterface; +use League\Uri\Exceptions\MissingFeature; +use League\Uri\Idna\Converter as IdnaConverter; +use League\Uri\IPv4\Converter as IPv4Converter; +use League\Uri\IPv6\Converter as IPv6Converter; +use Psr\Http\Message\UriFactoryInterface; +use Psr\Http\Message\UriInterface as Psr7UriInterface; +use Stringable; + +use function array_pop; +use function array_reduce; +use function count; +use function end; +use function explode; +use function implode; +use function in_array; +use function preg_match; +use function rawurldecode; +use function str_repeat; +use function str_replace; +use function strpos; +use function substr; + +/** + * @phpstan-import-type ComponentMap from UriInterface + */ +class BaseUri implements Stringable, JsonSerializable, UriAccess +{ + /** @var array<string,int> */ + final protected const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1]; + + /** @var array<string,int> */ + final protected const DOT_SEGMENTS = ['.' => 1, '..' => 1]; + + protected readonly Psr7UriInterface|UriInterface|null $origin; + protected readonly ?string $nullValue; + + /** + * @param UriFactoryInterface|null $uriFactory Deprecated, will be removed in the next major release + */ + final protected function __construct( + protected readonly Psr7UriInterface|UriInterface $uri, + protected readonly ?UriFactoryInterface $uriFactory + ) { + $this->nullValue = $this->uri instanceof Psr7UriInterface ? '' : null; + $this->origin = $this->computeOrigin($this->uri, $this->nullValue); + } + + public static function from(Stringable|string $uri, ?UriFactoryInterface $uriFactory = null): static + { + return new static(static::formatHost(static::filterUri($uri, $uriFactory)), $uriFactory); + } + + public function withUriFactory(UriFactoryInterface $uriFactory): static + { + return new static($this->uri, $uriFactory); + } + + public function withoutUriFactory(): static + { + return new static($this->uri, null); + } + + public function getUri(): Psr7UriInterface|UriInterface + { + return $this->uri; + } + + public function getUriString(): string + { + return $this->uri->__toString(); + } + + public function jsonSerialize(): string + { + return $this->uri->__toString(); + } + + public function __toString(): string + { + return $this->uri->__toString(); + } + + public function origin(): ?self + { + return match (null) { + $this->origin => null, + default => new self($this->origin, $this->uriFactory), + }; + } + + /** + * Returns the Unix filesystem path. + * + * The method will return null if a scheme is present and is not the `file` scheme + */ + public function unixPath(): ?string + { + return match ($this->uri->getScheme()) { + 'file', $this->nullValue => rawurldecode($this->uri->getPath()), + default => null, + }; + } + + /** + * Returns the Windows filesystem path. + * + * The method will return null if a scheme is present and is not the `file` scheme + */ + public function windowsPath(): ?string + { + static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),'; + + if (!in_array($this->uri->getScheme(), ['file', $this->nullValue], true)) { + return null; + } + + $originalPath = $this->uri->getPath(); + $path = $originalPath; + if ('/' === ($path[0] ?? '')) { + $path = substr($path, 1); + } + + if (1 === preg_match($regexpWindowsPath, $path, $matches)) { + $root = $matches['root']; + $path = substr($path, strlen($root)); + + return $root.str_replace('/', '\\', rawurldecode($path)); + } + + $host = $this->uri->getHost(); + + return match ($this->nullValue) { + $host => str_replace('/', '\\', rawurldecode($originalPath)), + default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)), + }; + } + + /** + * Returns a string representation of a File URI according to RFC8089. + * + * The method will return null if the URI scheme is not the `file` scheme + */ + public function toRfc8089(): ?string + { + $path = $this->uri->getPath(); + + return match (true) { + 'file' !== $this->uri->getScheme() => null, + in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => 'file:'.match (true) { + '' === $path, + '/' === $path[0] => $path, + default => '/'.$path, + }, + default => (string) $this->uri, + }; + } + + /** + * Tells whether the `file` scheme base URI represents a local file. + */ + public function isLocalFile(): bool + { + return match (true) { + 'file' !== $this->uri->getScheme() => false, + in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => true, + default => false, + }; + } + + /** + * Tells whether the URI is opaque or not. + * + * A URI is opaque if and only if it is absolute + * and does not has an authority path. + */ + public function isOpaque(): bool + { + return $this->nullValue === $this->uri->getAuthority() + && $this->isAbsolute(); + } + + /** + * Tells whether two URI do not share the same origin. + */ + public function isCrossOrigin(Stringable|string $uri): bool + { + if (null === $this->origin) { + return true; + } + + $uri = static::filterUri($uri); + $uriOrigin = $this->computeOrigin($uri, $uri instanceof Psr7UriInterface ? '' : null); + + return match(true) { + null === $uriOrigin, + $uriOrigin->__toString() !== $this->origin->__toString() => true, + default => false, + }; + } + + /** + * Tells whether the URI is absolute. + */ + public function isAbsolute(): bool + { + return $this->nullValue !== $this->uri->getScheme(); + } + + /** + * Tells whether the URI is a network path. + */ + public function isNetworkPath(): bool + { + return $this->nullValue === $this->uri->getScheme() + && $this->nullValue !== $this->uri->getAuthority(); + } + + /** + * Tells whether the URI is an absolute path. + */ + public function isAbsolutePath(): bool + { + return $this->nullValue === $this->uri->getScheme() + && $this->nullValue === $this->uri->getAuthority() + && '/' === ($this->uri->getPath()[0] ?? ''); + } + + /** + * Tells whether the URI is a relative path. + */ + public function isRelativePath(): bool + { + return $this->nullValue === $this->uri->getScheme() + && $this->nullValue === $this->uri->getAuthority() + && '/' !== ($this->uri->getPath()[0] ?? ''); + } + + /** + * Tells whether both URI refers to the same document. + */ + public function isSameDocument(Stringable|string $uri): bool + { + return $this->normalize(static::filterUri($uri)) === $this->normalize($this->uri); + } + + /** + * Tells whether the URI contains an Internationalized Domain Name (IDN). + */ + public function hasIdn(): bool + { + return IdnaConverter::isIdn($this->uri->getHost()); + } + + /** + * Tells whether the URI contains an IPv4 regardless if it is mapped or native. + */ + public function hasIPv4(): bool + { + return IPv4Converter::fromEnvironment()->isIpv4($this->uri->getHost()); + } + + /** + * Resolves a URI against a base URI using RFC3986 rules. + * + * This method MUST retain the state of the submitted URI instance, and return + * a URI instance of the same type that contains the applied modifications. + * + * This method MUST be transparent when dealing with error and exceptions. + * It MUST not alter or silence them apart from validating its own parameters. + */ + public function resolve(Stringable|string $uri): static + { + $uri = static::formatHost(static::filterUri($uri, $this->uriFactory)); + $null = $uri instanceof Psr7UriInterface ? '' : null; + + if ($null !== $uri->getScheme()) { + return new static( + $uri->withPath(static::removeDotSegments($uri->getPath())), + $this->uriFactory + ); + } + + if ($null !== $uri->getAuthority()) { + return new static( + $uri + ->withScheme($this->uri->getScheme()) + ->withPath(static::removeDotSegments($uri->getPath())), + $this->uriFactory + ); + } + + $user = $null; + $pass = null; + $userInfo = $this->uri->getUserInfo(); + if (null !== $userInfo) { + [$user, $pass] = explode(':', $userInfo, 2) + [1 => null]; + } + + [$path, $query] = $this->resolvePathAndQuery($uri); + + return new static( + $uri + ->withPath($this->removeDotSegments($path)) + ->withQuery($query) + ->withHost($this->uri->getHost()) + ->withPort($this->uri->getPort()) + ->withUserInfo($user, $pass) + ->withScheme($this->uri->getScheme()), + $this->uriFactory + ); + } + + /** + * Relativize a URI according to a base URI. + * + * This method MUST retain the state of the submitted URI instance, and return + * a URI instance of the same type that contains the applied modifications. + * + * This method MUST be transparent when dealing with error and exceptions. + * It MUST not alter of silence them apart from validating its own parameters. + */ + public function relativize(Stringable|string $uri): static + { + $uri = static::formatHost(static::filterUri($uri, $this->uriFactory)); + if ($this->canNotBeRelativize($uri)) { + return new static($uri, $this->uriFactory); + } + + $null = $uri instanceof Psr7UriInterface ? '' : null; + $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null); + $targetPath = $uri->getPath(); + $basePath = $this->uri->getPath(); + + return new static( + match (true) { + $targetPath !== $basePath => $uri->withPath(static::relativizePath($targetPath, $basePath)), + static::componentEquals('query', $uri) => $uri->withPath('')->withQuery($null), + $null === $uri->getQuery() => $uri->withPath(static::formatPathWithEmptyBaseQuery($targetPath)), + default => $uri->withPath(''), + }, + $this->uriFactory + ); + } + + final protected function computeOrigin(Psr7UriInterface|UriInterface $uri, ?string $nullValue): Psr7UriInterface|UriInterface|null + { + $scheme = $uri->getScheme(); + if ('blob' !== $scheme) { + return match (true) { + isset(static::WHATWG_SPECIAL_SCHEMES[$scheme]) => $uri + ->withFragment($nullValue) + ->withQuery($nullValue) + ->withPath('') + ->withUserInfo($nullValue), + default => null, + }; + } + + $components = UriString::parse($uri->getPath()); + if ($uri instanceof Psr7UriInterface) { + /** @var ComponentMap $components */ + $components = array_map(fn ($component) => null === $component ? '' : $component, $components); + } + + return match (true) { + null !== $components['scheme'] && isset(static::WHATWG_SPECIAL_SCHEMES[strtolower($components['scheme'])]) => $uri + ->withFragment($nullValue) + ->withQuery($nullValue) + ->withPath('') + ->withHost($components['host']) + ->withPort($components['port']) + ->withScheme($components['scheme']) + ->withUserInfo($nullValue), + default => null, + }; + } + + /** + * Normalizes a URI for comparison; this URI string representation is not suitable for usage as per RFC guidelines. + */ + final protected function normalize(Psr7UriInterface|UriInterface $uri): string + { + $null = $uri instanceof Psr7UriInterface ? '' : null; + + $path = $uri->getPath(); + if ('/' === ($path[0] ?? '') || '' !== $uri->getScheme().$uri->getAuthority()) { + $path = $this->removeDotSegments($path); + } + + $query = $uri->getQuery(); + $pairs = null === $query ? [] : explode('&', $query); + sort($pairs); + + static $regexpEncodedChars = ',%(2[D|E]|3\d|4[1-9|A-F]|5[\d|AF]|6[1-9|A-F]|7[\d|E]),i'; + $value = preg_replace_callback( + $regexpEncodedChars, + static fn (array $matches): string => rawurldecode($matches[0]), + [$path, implode('&', $pairs)] + ) ?? ['', $null]; + + [$path, $query] = $value + ['', $null]; + if ($null !== $uri->getAuthority() && '' === $path) { + $path = '/'; + } + + return $uri + ->withHost(Uri::fromComponents(['host' => $uri->getHost()])->getHost()) + ->withPath($path) + ->withQuery([] === $pairs ? $null : $query) + ->withFragment($null) + ->__toString(); + } + + /** + * Input URI normalization to allow Stringable and string URI. + */ + final protected static function filterUri(Stringable|string $uri, UriFactoryInterface|null $uriFactory = null): Psr7UriInterface|UriInterface + { + return match (true) { + $uri instanceof UriAccess => $uri->getUri(), + $uri instanceof Psr7UriInterface, + $uri instanceof UriInterface => $uri, + $uriFactory instanceof UriFactoryInterface => $uriFactory->createUri((string) $uri), + default => Uri::new($uri), + }; + } + + /** + * Remove dot segments from the URI path as per RFC specification. + */ + final protected function removeDotSegments(string $path): string + { + if (!str_contains($path, '.')) { + return $path; + } + + $reducer = function (array $carry, string $segment): array { + if ('..' === $segment) { + array_pop($carry); + + return $carry; + } + + if (!isset(static::DOT_SEGMENTS[$segment])) { + $carry[] = $segment; + } + + return $carry; + }; + + $oldSegments = explode('/', $path); + $newPath = implode('/', array_reduce($oldSegments, $reducer(...), [])); + if (isset(static::DOT_SEGMENTS[end($oldSegments)])) { + $newPath .= '/'; + } + + // @codeCoverageIgnoreStart + // added because some PSR-7 implementations do not respect RFC3986 + if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) { + return '/'.$newPath; + } + // @codeCoverageIgnoreEnd + + return $newPath; + } + + /** + * Resolves an URI path and query component. + * + * @return array{0:string, 1:string|null} + */ + final protected function resolvePathAndQuery(Psr7UriInterface|UriInterface $uri): array + { + $targetPath = $uri->getPath(); + $null = $uri instanceof Psr7UriInterface ? '' : null; + + if (str_starts_with($targetPath, '/')) { + return [$targetPath, $uri->getQuery()]; + } + + if ('' === $targetPath) { + $targetQuery = $uri->getQuery(); + if ($null === $targetQuery) { + $targetQuery = $this->uri->getQuery(); + } + + $targetPath = $this->uri->getPath(); + //@codeCoverageIgnoreStart + //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction + if (null !== $this->uri->getAuthority() && !str_starts_with($targetPath, '/')) { + $targetPath = '/'.$targetPath; + } + //@codeCoverageIgnoreEnd + + return [$targetPath, $targetQuery]; + } + + $basePath = $this->uri->getPath(); + if (null !== $this->uri->getAuthority() && '' === $basePath) { + $targetPath = '/'.$targetPath; + } + + if ('' !== $basePath) { + $segments = explode('/', $basePath); + array_pop($segments); + if ([] !== $segments) { + $targetPath = implode('/', $segments).'/'.$targetPath; + } + } + + return [$targetPath, $uri->getQuery()]; + } + + /** + * Tells whether the component value from both URI object equals. + * + * @pqram 'query'|'authority'|'scheme' $property + */ + final protected function componentEquals(string $property, Psr7UriInterface|UriInterface $uri): bool + { + $getComponent = function (string $property, Psr7UriInterface|UriInterface $uri): ?string { + $component = match ($property) { + 'query' => $uri->getQuery(), + 'authority' => $uri->getAuthority(), + default => $uri->getScheme(), + }; + + return match (true) { + $uri instanceof UriInterface, '' !== $component => $component, + default => null, + }; + }; + + return $getComponent($property, $uri) === $getComponent($property, $this->uri); + } + + /** + * Filter the URI object. + */ + final protected static function formatHost(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface + { + $host = $uri->getHost(); + try { + $converted = IPv4Converter::fromEnvironment()->toDecimal($host); + } catch (MissingFeature) { + $converted = null; + } + + if (false === filter_var($converted, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $converted = IPv6Converter::compress($host); + } + + return match (true) { + null !== $converted => $uri->withHost($converted), + '' === $host, + $uri instanceof UriInterface => $uri, + default => $uri->withHost((string) Uri::fromComponents(['host' => $host])->getHost()), + }; + } + + /** + * Tells whether the submitted URI object can be relativized. + */ + final protected function canNotBeRelativize(Psr7UriInterface|UriInterface $uri): bool + { + return !static::componentEquals('scheme', $uri) + || !static::componentEquals('authority', $uri) + || static::from($uri)->isRelativePath(); + } + + /** + * Relatives the URI for an authority-less target URI. + */ + final protected static function relativizePath(string $path, string $basePath): string + { + $baseSegments = static::getSegments($basePath); + $targetSegments = static::getSegments($path); + $targetBasename = array_pop($targetSegments); + array_pop($baseSegments); + foreach ($baseSegments as $offset => $segment) { + if (!isset($targetSegments[$offset]) || $segment !== $targetSegments[$offset]) { + break; + } + unset($baseSegments[$offset], $targetSegments[$offset]); + } + $targetSegments[] = $targetBasename; + + return static::formatPath( + str_repeat('../', count($baseSegments)).implode('/', $targetSegments), + $basePath + ); + } + + /** + * returns the path segments. + * + * @return string[] + */ + final protected static function getSegments(string $path): array + { + return explode('/', match (true) { + '' === $path, + '/' !== $path[0] => $path, + default => substr($path, 1), + }); + } + + /** + * Formatting the path to keep a valid URI. + */ + final protected static function formatPath(string $path, string $basePath): string + { + $colonPosition = strpos($path, ':'); + $slashPosition = strpos($path, '/'); + + return match (true) { + '' === $path => match (true) { + '' === $basePath, + '/' === $basePath => $basePath, + default => './', + }, + false === $colonPosition => $path, + false === $slashPosition, + $colonPosition < $slashPosition => "./$path", + default => $path, + }; + } + + /** + * Formatting the path to keep a resolvable URI. + */ + final protected static function formatPathWithEmptyBaseQuery(string $path): string + { + $targetSegments = static::getSegments($path); + /** @var string $basename */ + $basename = end($targetSegments); + + return '' === $basename ? './' : $basename; + } +} diff --git a/vendor/league/uri/Http.php b/vendor/league/uri/Http.php new file mode 100644 index 000000000..0293a9864 --- /dev/null +++ b/vendor/league/uri/Http.php @@ -0,0 +1,327 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use Deprecated; +use JsonSerializable; +use League\Uri\Contracts\UriException; +use League\Uri\Contracts\UriInterface; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\UriTemplate\TemplateCanNotBeExpanded; +use Psr\Http\Message\UriInterface as Psr7UriInterface; +use Stringable; + +/** + * @phpstan-import-type InputComponentMap from UriString + */ +final class Http implements Stringable, Psr7UriInterface, JsonSerializable +{ + private readonly UriInterface $uri; + + private function __construct(UriInterface $uri) + { + if (null === $uri->getScheme() && '' === $uri->getHost()) { + throw new SyntaxError('An URI without scheme cannot contain an empty host string according to PSR-7: '.$uri); + } + + $port = $uri->getPort(); + if (null !== $port && ($port < 0 || $port > 65535)) { + throw new SyntaxError('The URI port is outside the established TCP and UDP port ranges: '.$uri); + } + + $this->uri = $this->normalizePsr7Uri($uri); + } + + /** + * PSR-7 UriInterface makes the following normalization. + * + * Safely stringify input when possible for League UriInterface compatibility. + * + * Query, Fragment and User Info when undefined are normalized to the empty string + */ + private function normalizePsr7Uri(UriInterface $uri): UriInterface + { + $components = []; + if ('' === $uri->getFragment()) { + $components['fragment'] = null; + } + + if ('' === $uri->getQuery()) { + $components['query'] = null; + } + + if ('' === $uri->getUserInfo()) { + $components['user'] = null; + $components['pass'] = null; + } + + return match ($components) { + [] => $uri, + default => Uri::fromComponents([...$uri->toComponents(), ...$components]), + }; + } + + /** + * Create a new instance from a string or a stringable object. + */ + public static function new(Stringable|string $uri = ''): self + { + return self::fromComponents(UriString::parse($uri)); + } + + /** + * Create a new instance from a hash of parse_url parts. + * + * @param InputComponentMap $components a hash representation of the URI similar + * to PHP parse_url function result + */ + public static function fromComponents(array $components): self + { + $components += [ + 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, + 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, + ]; + + if ('' === $components['user']) { + $components['user'] = null; + } + + if ('' === $components['pass']) { + $components['pass'] = null; + } + + if ('' === $components['query']) { + $components['query'] = null; + } + + if ('' === $components['fragment']) { + $components['fragment'] = null; + } + + return new self(Uri::fromComponents($components)); + } + + /** + * Create a new instance from the environment. + */ + public static function fromServer(array $server): self + { + return new self(Uri::fromServer($server)); + } + + /** + * Create a new instance from a URI and a Base URI. + * + * The returned URI must be absolute. + */ + public static function fromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self + { + return new self(Uri::fromBaseUri($uri, $baseUri)); + } + + /** + * Creates a new instance from a template. + * + * @throws TemplateCanNotBeExpanded if the variables are invalid or missing + * @throws UriException if the variables are invalid or missing + */ + public static function fromTemplate(Stringable|string $template, iterable $variables = []): self + { + return new self(Uri::fromTemplate($template, $variables)); + } + + public function getScheme(): string + { + return $this->uri->getScheme() ?? ''; + } + + public function getAuthority(): string + { + return $this->uri->getAuthority() ?? ''; + } + + public function getUserInfo(): string + { + return $this->uri->getUserInfo() ?? ''; + } + + public function getHost(): string + { + return $this->uri->getHost() ?? ''; + } + + public function getPort(): ?int + { + return $this->uri->getPort(); + } + + public function getPath(): string + { + return $this->uri->getPath(); + } + + public function getQuery(): string + { + return $this->uri->getQuery() ?? ''; + } + + public function getFragment(): string + { + return $this->uri->getFragment() ?? ''; + } + + public function __toString(): string + { + return $this->uri->toString(); + } + + public function jsonSerialize(): string + { + return $this->uri->toString(); + } + + /** + * Safely stringify input when possible for League UriInterface compatibility. + */ + private function filterInput(string $str): ?string + { + return match ('') { + $str => null, + default => $str, + }; + } + + private function newInstance(UriInterface $uri): self + { + return match ($this->uri->toString()) { + $uri->toString() => $this, + default => new self($uri), + }; + } + + public function withScheme(string $scheme): self + { + return $this->newInstance($this->uri->withScheme($this->filterInput($scheme))); + } + + public function withUserInfo(string $user, ?string $password = null): self + { + return $this->newInstance($this->uri->withUserInfo($this->filterInput($user), $password)); + } + + public function withHost(string $host): self + { + return $this->newInstance($this->uri->withHost($this->filterInput($host))); + } + + public function withPort(?int $port): self + { + return $this->newInstance($this->uri->withPort($port)); + } + + public function withPath(string $path): self + { + return $this->newInstance($this->uri->withPath($path)); + } + + public function withQuery(string $query): self + { + return $this->newInstance($this->uri->withQuery($this->filterInput($query))); + } + + public function withFragment(string $fragment): self + { + return $this->newInstance($this->uri->withFragment($this->filterInput($fragment))); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Http::new() + * + * Create a new instance from a string. + */ + #[Deprecated(message:'use League\Uri\Http::new() instead', since:'league/uri:7.0.0')] + public static function createFromString(Stringable|string $uri = ''): self + { + return self::new($uri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Http::fromComponents() + * + * Create a new instance from a hash of parse_url parts. + * + * @param InputComponentMap $components a hash representation of the URI similar + * to PHP parse_url function result + */ + #[Deprecated(message:'use League\Uri\Http::fromComponents() instead', since:'league/uri:7.0.0')] + public static function createFromComponents(array $components): self + { + return self::fromComponents($components); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Http::fromServer() + * + * Create a new instance from the environment. + */ + #[Deprecated(message:'use League\Uri\Http::fromServer() instead', since:'league/uri:7.0.0')] + public static function createFromServer(array $server): self + { + return self::fromServer($server); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Http::new() + * + * Create a new instance from a URI object. + */ + #[Deprecated(message:'use League\Uri\Http::new() instead', since:'league/uri:7.0.0')] + public static function createFromUri(Psr7UriInterface|UriInterface $uri): self + { + return self::new($uri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Http::fromBaseUri() + * + * Create a new instance from a URI and a Base URI. + * + * The returned URI must be absolute. + */ + #[Deprecated(message:'use League\Uri\Http::fromBaseUri() instead', since:'league/uri:7.0.0')] + public static function createFromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self + { + return self::fromBaseUri($uri, $baseUri); + } +} diff --git a/vendor/league/uri/HttpFactory.php b/vendor/league/uri/HttpFactory.php new file mode 100644 index 000000000..3508e67cc --- /dev/null +++ b/vendor/league/uri/HttpFactory.php @@ -0,0 +1,25 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use Psr\Http\Message\UriFactoryInterface; +use Psr\Http\Message\UriInterface; + +final class HttpFactory implements UriFactoryInterface +{ + public function createUri(string $uri = ''): UriInterface + { + return Http::new($uri); + } +} diff --git a/vendor/league/uri/LICENSE b/vendor/league/uri/LICENSE new file mode 100644 index 000000000..3b52528f2 --- /dev/null +++ b/vendor/league/uri/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 ignace nyamagana butera + +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. diff --git a/vendor/league/uri/Uri.php b/vendor/league/uri/Uri.php new file mode 100644 index 000000000..00383428d --- /dev/null +++ b/vendor/league/uri/Uri.php @@ -0,0 +1,1328 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use Deprecated; +use finfo; +use League\Uri\Contracts\UriComponentInterface; +use League\Uri\Contracts\UriException; +use League\Uri\Contracts\UriInterface; +use League\Uri\Exceptions\ConversionFailed; +use League\Uri\Exceptions\MissingFeature; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\Idna\Converter as IdnConverter; +use League\Uri\UriTemplate\TemplateCanNotBeExpanded; +use Psr\Http\Message\UriInterface as Psr7UriInterface; +use SensitiveParameter; +use Stringable; + +use function array_filter; +use function array_map; +use function base64_decode; +use function base64_encode; +use function count; +use function explode; +use function file_get_contents; +use function filter_var; +use function implode; +use function in_array; +use function inet_pton; +use function ltrim; +use function preg_match; +use function preg_replace_callback; +use function rawurlencode; +use function str_contains; +use function str_replace; +use function strlen; +use function strpos; +use function strspn; +use function strtolower; +use function substr; + +use const FILEINFO_MIME; +use const FILTER_FLAG_IPV4; +use const FILTER_FLAG_IPV6; +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_BOOLEAN; +use const FILTER_VALIDATE_IP; + +/** + * @phpstan-import-type ComponentMap from UriString + * @phpstan-import-type InputComponentMap from UriString + */ +final class Uri implements UriInterface +{ + /** + * RFC3986 invalid characters. + * + * @link https://tools.ietf.org/html/rfc3986#section-2.2 + * + * @var string + */ + private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/'; + + /** + * RFC3986 schema regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.1 + * + * @var string + */ + private const REGEXP_SCHEME = ',^[a-z]([-a-z\d+.]+)?$,i'; + + /** + * RFC3986 host identified by a registered name regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @var string + */ + private const REGEXP_HOST_REGNAME = '/^( + (?<unreserved>[a-z\d_~\-\.])| + (?<sub_delims>[!$&\'()*+,;=])| + (?<encoded>%[A-F\d]{2}) + )+$/x'; + + /** + * RFC3986 delimiters of the generic URI components regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc3986#section-2.2 + * + * @var string + */ + private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space. + + /** + * RFC3986 IPvFuture regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @var string + */ + private const REGEXP_HOST_IP_FUTURE = '/^ + v(?<version>[A-F\d])+\. + (?: + (?<unreserved>[a-z\d_~\-\.])| + (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character + )+ + $/ix'; + + /** + * RFC3986 IPvFuture host and port component. + * + * @var string + */ + private const REGEXP_HOST_PORT = ',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x'; + + /** + * Significant 10 bits of IP to detect Zone ID regular expression pattern. + * + * @var string + */ + private const HOST_ADDRESS_BLOCK = "\xfe\x80"; + + /** + * Regular expression pattern to for file URI. + * <volume> contains the volume but not the volume separator. + * The volume separator may be URL-encoded (`|` as `%7C`) by ::formatPath(), + * so we account for that here. + * + * @var string + */ + private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<volume>[a-zA-Z])(?:[:|\|]|%7C)(?<rest>.*)?,'; + + /** + * Mimetype regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc2397 + * + * @var string + */ + private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,'; + + /** + * Base64 content regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc2397 + * + * @var string + */ + private const REGEXP_BINARY = ',(;|^)base64$,'; + + /** + * Windows file path string regular expression pattern. + * <root> contains both the volume and volume separator. + * + * @var string + */ + private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),'; + + /** + * Supported schemes and corresponding default port. + * + * @var array<string, int|null> + */ + private const SCHEME_DEFAULT_PORT = [ + 'data' => null, + 'file' => null, + 'ftp' => 21, + 'gopher' => 70, + 'http' => 80, + 'https' => 443, + 'ws' => 80, + 'wss' => 443, + ]; + + /** + * Maximum number of cached items. + * + * @var int + */ + private const MAXIMUM_CACHED_ITEMS = 100; + + /** + * All ASCII letters sorted by typical frequency of occurrence. + * + * @var string + */ + private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; + + private readonly ?string $scheme; + private readonly ?string $user; + private readonly ?string $pass; + private readonly ?string $userInfo; + private readonly ?string $host; + private readonly ?int $port; + private readonly ?string $authority; + private readonly string $path; + private readonly ?string $query; + private readonly ?string $fragment; + private readonly string $uri; + + private function __construct( + ?string $scheme, + ?string $user, + #[SensitiveParameter] ?string $pass, + ?string $host, + ?int $port, + string $path, + ?string $query, + ?string $fragment + ) { + $this->scheme = $this->formatScheme($scheme); + $this->user = Encoder::encodeUser($user); + $this->pass = Encoder::encodePassword($pass); + $this->host = $this->formatHost($host); + $this->port = $this->formatPort($port); + $this->path = $this->formatPath($path); + $this->query = Encoder::encodeQueryOrFragment($query); + $this->fragment = Encoder::encodeQueryOrFragment($fragment); + $this->userInfo = $this->formatUserInfo($this->user, $this->pass); + $this->authority = UriString::buildAuthority($this->toComponents()); + $this->uri = UriString::buildUri($this->scheme, $this->authority, $this->path, $this->query, $this->fragment); + + $this->assertValidState(); + } + + /** + * Format the Scheme and Host component. + * + * @throws SyntaxError if the scheme is invalid + */ + private function formatScheme(?string $scheme): ?string + { + if (null === $scheme) { + return null; + } + + $formattedScheme = strtolower($scheme); + static $cache = []; + if (isset($cache[$formattedScheme])) { + return $formattedScheme; + } + + if ( + !array_key_exists($formattedScheme, self::SCHEME_DEFAULT_PORT) + && 1 !== preg_match(self::REGEXP_SCHEME, $formattedScheme) + ) { + throw new SyntaxError('The scheme `'.$scheme.'` is invalid.'); + } + + $cache[$formattedScheme] = 1; + if (self::MAXIMUM_CACHED_ITEMS < count($cache)) { + array_shift($cache); + } + + return $formattedScheme; + } + + /** + * Set the UserInfo component. + */ + private function formatUserInfo( + ?string $user, + #[SensitiveParameter] ?string $password + ): ?string { + return match (null) { + $password => $user, + default => $user.':'.$password, + }; + } + + /** + * Validate and Format the Host component. + */ + private function formatHost(?string $host): ?string + { + if (null === $host || '' === $host) { + return $host; + } + + static $cache = []; + if (isset($cache[$host])) { + return $cache[$host]; + } + + $formattedHost = '[' === $host[0] ? $this->formatIp($host) : $this->formatRegisteredName($host); + $cache[$host] = $formattedHost; + if (self::MAXIMUM_CACHED_ITEMS < count($cache)) { + array_shift($cache); + } + + return $formattedHost; + } + + /** + * Validate and format a registered name. + * + * The host is converted to its ascii representation if needed + * + * @throws MissingFeature if the submitted host required missing or misconfigured IDN support + * @throws SyntaxError if the submitted host is not a valid registered name + * @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form + */ + private function formatRegisteredName(string $host): string + { + $formattedHost = rawurldecode($host); + + return match (1) { + preg_match(self::REGEXP_HOST_REGNAME, $formattedHost) => $formattedHost, + preg_match(self::REGEXP_HOST_GEN_DELIMS, $formattedHost) => throw new SyntaxError('The host `'.$host.'` is invalid : a registered name cannot contain URI delimiters or spaces.'), + default => IdnConverter::toAsciiOrFail($host), + }; + } + + /** + * Validate and Format the IPv6/IPvfuture host. + * + * @throws SyntaxError if the submitted host is not a valid IP host + */ + private function formatIp(string $host): string + { + $ip = substr($host, 1, -1); + if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return $host; + } + + if (1 === preg_match(self::REGEXP_HOST_IP_FUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) { + return $host; + } + + $pos = strpos($ip, '%'); + if (false === $pos) { + throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); + } + + if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) { + throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); + } + + $ip = substr($ip, 0, $pos); + if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); + } + + //Only the address block fe80::/10 can have a Zone ID attach to + //let's detect the link local significant 10 bits + if (str_starts_with((string)inet_pton($ip), self::HOST_ADDRESS_BLOCK)) { + return $host; + } + + throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); + } + + /** + * Format the Port component. + * + * @throws SyntaxError + */ + private function formatPort(?int $port = null): ?int + { + $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null; + + return match (true) { + null === $port, $defaultPort === $port => null, + 0 > $port => throw new SyntaxError('The port `'.$port.'` is invalid.'), + default => $port, + }; + } + + /** + * Create a new instance from a string. + */ + public static function new(Stringable|string $uri = ''): self + { + $components = match (true) { + $uri instanceof UriInterface => $uri->toComponents(), + default => UriString::parse($uri), + }; + + return new self( + $components['scheme'], + $components['user'], + $components['pass'], + $components['host'], + $components['port'], + $components['path'], + $components['query'], + $components['fragment'] + ); + } + + /** + * Creates a new instance from a URI and a Base URI. + * + * The returned URI must be absolute. + */ + public static function fromBaseUri( + Stringable|string $uri, + Stringable|string|null $baseUri = null + ): self { + $uri = self::new($uri); + $baseUri = BaseUri::from($baseUri ?? $uri); + + /** @var self $uri */ + $uri = match (true) { + $baseUri->isAbsolute() => $baseUri->resolve($uri)->getUri(), + default => throw new SyntaxError('the URI `'.$baseUri.'` must be absolute.'), + }; + + return $uri; + } + + /** + * Creates a new instance from a template. + * + * @throws TemplateCanNotBeExpanded if the variables are invalid or missing + * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance + */ + public static function fromTemplate(UriTemplate|Stringable|string $template, iterable $variables = []): self + { + return match (true) { + $template instanceof UriTemplate => self::fromComponents($template->expand($variables)->toComponents()), + $template instanceof UriTemplate\Template => self::new($template->expand($variables)), + default => self::new(UriTemplate\Template::new($template)->expand($variables)), + }; + } + + /** + * Create a new instance from a hash representation of the URI similar + * to PHP parse_url function result. + * + * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result + */ + public static function fromComponents(array $components = []): self + { + $components += [ + 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, + 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, + ]; + + if (null === $components['path']) { + $components['path'] = ''; + } + + return new self( + $components['scheme'], + $components['user'], + $components['pass'], + $components['host'], + $components['port'], + $components['path'], + $components['query'], + $components['fragment'] + ); + } + + /** + * Create a new instance from a data file path. + * + * @param resource|null $context + * + * @throws MissingFeature If ext/fileinfo is not installed + * @throws SyntaxError If the file does not exist or is not readable + */ + public static function fromFileContents(Stringable|string $path, $context = null): self + { + FeatureDetection::supportsFileDetection(); + + $path = (string) $path; + $fileArguments = [$path, false]; + $mimeArguments = [$path, FILEINFO_MIME]; + if (null !== $context) { + $fileArguments[] = $context; + $mimeArguments[] = $context; + } + + set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true); + $raw = file_get_contents(...$fileArguments); + restore_error_handler(); + + if (false === $raw) { + throw new SyntaxError('The file `'.$path.'` does not exist or is not readable.'); + } + + $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mimeArguments); + + return Uri::fromComponents([ + 'scheme' => 'data', + 'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)), + ]); + } + + /** + * Create a new instance from a data URI string. + * + * @throws SyntaxError If the parameter syntax is invalid + */ + public static function fromData(string $data, string $mimetype = '', string $parameters = ''): self + { + static $regexpMimetype = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,'; + + $mimetype = match (true) { + '' === $mimetype => 'text/plain', + 1 === preg_match($regexpMimetype, $mimetype) => $mimetype, + default => throw new SyntaxError('Invalid mimeType, `'.$mimetype.'`.'), + }; + + if ('' === $parameters) { + return self::fromComponents([ + 'scheme' => 'data', + 'path' => self::formatDataPath($mimetype.','.rawurlencode($data)), + ]); + } + + $isInvalidParameter = static function (string $parameter): bool { + $properties = explode('=', $parameter); + + return 2 !== count($properties) || 'base64' === strtolower($properties[0]); + }; + + if (str_starts_with($parameters, ';')) { + $parameters = substr($parameters, 1); + } + + return match ([]) { + array_filter(explode(';', $parameters), $isInvalidParameter) => self::fromComponents([ + 'scheme' => 'data', + 'path' => self::formatDataPath($mimetype.';'.$parameters.','.rawurlencode($data)), + ]), + default => throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters)) + }; + } + + /** + * Create a new instance from a Unix path string. + */ + public static function fromUnixPath(Stringable|string $path): self + { + $path = implode('/', array_map(rawurlencode(...), explode('/', (string) $path))); + + return Uri::fromComponents(match (true) { + '/' !== ($path[0] ?? '') => ['path' => $path], + default => ['path' => $path, 'scheme' => 'file', 'host' => ''], + }); + } + + /** + * Create a new instance from a local Windows path string. + */ + public static function fromWindowsPath(Stringable|string $path): self + { + $path = (string) $path; + $root = ''; + if (1 === preg_match(self::REGEXP_WINDOW_PATH, $path, $matches)) { + $root = substr($matches['root'], 0, -1).':'; + $path = substr($path, strlen($root)); + } + $path = str_replace('\\', '/', $path); + $path = implode('/', array_map(rawurlencode(...), explode('/', $path))); + + //Local Windows absolute path + if ('' !== $root) { + return Uri::fromComponents(['path' => '/'.$root.$path, 'scheme' => 'file', 'host' => '']); + } + + //UNC Windows Path + if (!str_starts_with($path, '//')) { + return Uri::fromComponents(['path' => $path]); + } + + [$host, $path] = explode('/', substr($path, 2), 2) + [1 => '']; + + return Uri::fromComponents(['host' => $host, 'path' => '/'.$path, 'scheme' => 'file']); + } + + /** + * Creates a new instance from a RFC8089 compatible URI. + * + * @see https://datatracker.ietf.org/doc/html/rfc8089 + */ + public static function fromRfc8089(Stringable|string $uri): UriInterface + { + $fileUri = self::new((string) preg_replace(',^(file:/)([^/].*)$,i', 'file:///$2', (string) $uri)); + $scheme = $fileUri->getScheme(); + + return match (true) { + 'file' !== $scheme => throw new SyntaxError('As per RFC8089, the URI scheme must be `file`.'), + 'localhost' === $fileUri->getAuthority() => $fileUri->withHost(''), + default => $fileUri, + }; + } + + /** + * Create a new instance from the environment. + */ + public static function fromServer(array $server): self + { + $components = ['scheme' => self::fetchScheme($server)]; + [$components['user'], $components['pass']] = self::fetchUserInfo($server); + [$components['host'], $components['port']] = self::fetchHostname($server); + [$components['path'], $components['query']] = self::fetchRequestUri($server); + + return Uri::fromComponents($components); + } + + /** + * Returns the environment scheme. + */ + private static function fetchScheme(array $server): string + { + $server += ['HTTPS' => '']; + + return match (true) { + false !== filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) => 'https', + default => 'http', + }; + } + + /** + * Returns the environment user info. + * + * @return non-empty-array{0: ?string, 1: ?string} + */ + private static function fetchUserInfo(array $server): array + { + $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => '']; + $user = $server['PHP_AUTH_USER']; + $pass = $server['PHP_AUTH_PW']; + if (str_starts_with(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) { + $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true); + if (false === $userinfo) { + throw new SyntaxError('The user info could not be detected'); + } + [$user, $pass] = explode(':', $userinfo, 2) + [1 => null]; + } + + if (null !== $user) { + $user = rawurlencode($user); + } + + if (null !== $pass) { + $pass = rawurlencode($pass); + } + + return [$user, $pass]; + } + + /** + * Returns the environment host. + * + * @throws SyntaxError If the host cannot be detected + * + * @return array{0:string|null, 1:int|null} + */ + private static function fetchHostname(array $server): array + { + $server += ['SERVER_PORT' => null]; + if (null !== $server['SERVER_PORT']) { + $server['SERVER_PORT'] = (int) $server['SERVER_PORT']; + } + + if (isset($server['HTTP_HOST']) && 1 === preg_match(self::REGEXP_HOST_PORT, $server['HTTP_HOST'], $matches)) { + $matches += ['host' => null, 'port' => null]; + if (null !== $matches['port']) { + $matches['port'] = (int) $matches['port']; + } + + return [$matches['host'], $matches['port'] ?? $server['SERVER_PORT']]; + } + + if (!isset($server['SERVER_ADDR'])) { + throw new SyntaxError('The host could not be detected'); + } + + if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return ['['.$server['SERVER_ADDR'].']', $server['SERVER_PORT']]; + } + + return [$server['SERVER_ADDR'], $server['SERVER_PORT']]; + } + + /** + * Returns the environment path. + * + * @return list<?string> + */ + private static function fetchRequestUri(array $server): array + { + $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null]; + if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) { + return explode('?', $server['UNENCODED_URL'], 2) + [1 => null]; + } + + if (isset($server['REQUEST_URI'])) { + [$path] = explode('?', $server['REQUEST_URI'], 2); + $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null; + + return [$path, $query]; + } + + return [$server['PHP_SELF'], $server['QUERY_STRING']]; + } + + /** + * Format the Path component. + */ + private function formatPath(string $path): string + { + return match ($this->scheme) { + 'data' => Encoder::encodePath(self::formatDataPath($path)), + 'file' => $this->formatFilePath(Encoder::encodePath($path)), + default => Encoder::encodePath($path), + }; + } + + /** + * Filter the Path component. + * + * @link https://tools.ietf.org/html/rfc2397 + * + * @throws SyntaxError If the path is not compliant with RFC2397 + */ + private static function formatDataPath(string $path): string + { + if ('' == $path) { + return 'text/plain;charset=us-ascii,'; + } + + if (strlen($path) !== strspn($path, self::ASCII) || !str_contains($path, ',')) { + throw new SyntaxError('The path `'.$path.'` is invalid according to RFC2937.'); + } + + $parts = explode(',', $path, 2) + [1 => null]; + $mediatype = explode(';', (string) $parts[0], 2) + [1 => null]; + $data = (string) $parts[1]; + $mimetype = $mediatype[0]; + if (null === $mimetype || '' === $mimetype) { + $mimetype = 'text/plain'; + } + + $parameters = $mediatype[1]; + if (null === $parameters || '' === $parameters) { + $parameters = 'charset=us-ascii'; + } + + self::assertValidPath($mimetype, $parameters, $data); + + return $mimetype.';'.$parameters.','.$data; + } + + /** + * Assert the path is a compliant with RFC2397. + * + * @link https://tools.ietf.org/html/rfc2397 + * + * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397 + */ + private static function assertValidPath(string $mimetype, string $parameters, string $data): void + { + if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) { + throw new SyntaxError('The path mimetype `'.$mimetype.'` is invalid.'); + } + + $isBinary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches); + if ($isBinary) { + $parameters = substr($parameters, 0, - strlen($matches[0])); + } + + $res = array_filter(array_filter(explode(';', $parameters), self::validateParameter(...))); + if ([] !== $res) { + throw new SyntaxError('The path parameters `'.$parameters.'` is invalid.'); + } + + if (!$isBinary) { + return; + } + + $res = base64_decode($data, true); + if (false === $res || $data !== base64_encode($res)) { + throw new SyntaxError('The path data `'.$data.'` is invalid.'); + } + } + + /** + * Validate mediatype parameter. + */ + private static function validateParameter(string $parameter): bool + { + $properties = explode('=', $parameter); + + return 2 != count($properties) || 'base64' === strtolower($properties[0]); + } + + /** + * Format path component for file scheme. + */ + private function formatFilePath(string $path): string + { + return (string) preg_replace_callback( + self::REGEXP_FILE_PATH, + static fn (array $matches): string => $matches['delim'].$matches['volume'].(isset($matches['rest']) ? ':'.$matches['rest'] : ''), + $path + ); + } + + /** + * assert the URI internal state is valid. + * + * @link https://tools.ietf.org/html/rfc3986#section-3 + * @link https://tools.ietf.org/html/rfc3986#section-3.3 + * + * @throws SyntaxError if the URI is in an invalid state according to RFC3986 + * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules + */ + private function assertValidState(): void + { + if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) { + throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.'); + } + + if (null === $this->authority && str_starts_with($this->path, '//')) { + throw new SyntaxError('If there is no authority the path `'.$this->path.'` cannot start with a `//`.'); + } + + $pos = strpos($this->path, ':'); + if (null === $this->authority + && null === $this->scheme + && false !== $pos + && !str_contains(substr($this->path, 0, $pos), '/') + ) { + throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.'); + } + + if (! match ($this->scheme) { + 'data' => $this->isUriWithSchemeAndPathOnly(), + 'file' => $this->isUriWithSchemeHostAndPathOnly(), + 'ftp', 'gopher' => $this->isNonEmptyHostUriWithoutFragmentAndQuery(), + 'http', 'https' => $this->isNonEmptyHostUri(), + 'ws', 'wss' => $this->isNonEmptyHostUriWithoutFragment(), + default => true, + }) { + throw new SyntaxError('The uri `'.$this->uri.'` is invalid for the `'.$this->scheme.'` scheme.'); + } + } + + /** + * URI validation for URI schemes which allows only scheme and path components. + */ + private function isUriWithSchemeAndPathOnly(): bool + { + return null === $this->authority + && null === $this->query + && null === $this->fragment; + } + + /** + * URI validation for URI schemes which allows only scheme, host and path components. + */ + private function isUriWithSchemeHostAndPathOnly(): bool + { + return null === $this->userInfo + && null === $this->port + && null === $this->query + && null === $this->fragment + && !('' != $this->scheme && null === $this->host); + } + + /** + * URI validation for URI schemes which disallow the empty '' host. + */ + private function isNonEmptyHostUri(): bool + { + return '' !== $this->host + && !(null !== $this->scheme && null === $this->host); + } + + /** + * URI validation for URIs schemes which disallow the empty '' host + * and forbids the fragment component. + */ + private function isNonEmptyHostUriWithoutFragment(): bool + { + return $this->isNonEmptyHostUri() && null === $this->fragment; + } + + /** + * URI validation for URIs schemes which disallow the empty '' host + * and forbids fragment and query components. + */ + private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool + { + return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query; + } + + public function toString(): string + { + return $this->uri; + } + + /** + * {@inheritDoc} + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): string + { + return $this->toString(); + } + + /** + * @return ComponentMap + */ + public function toComponents(): array + { + return [ + 'scheme' => $this->scheme, + 'user' => $this->user, + 'pass' => $this->pass, + 'host' => $this->host, + 'port' => $this->port, + 'path' => $this->path, + 'query' => $this->query, + 'fragment' => $this->fragment, + ]; + } + + /** + * {@inheritDoc} + */ + public function getScheme(): ?string + { + return $this->scheme; + } + + /** + * {@inheritDoc} + */ + public function getAuthority(): ?string + { + return $this->authority; + } + + /** + * {@inheritDoc} + */ + public function getUsername(): ?string + { + return $this->user; + } + + /** + * {@inheritDoc} + */ + public function getPassword(): ?string + { + return $this->pass; + } + + /** + * {@inheritDoc} + */ + public function getUserInfo(): ?string + { + return $this->userInfo; + } + + /** + * {@inheritDoc} + */ + public function getHost(): ?string + { + return $this->host; + } + + /** + * {@inheritDoc} + */ + public function getPort(): ?int + { + return $this->port; + } + + /** + * {@inheritDoc} + */ + public function getPath(): string + { + return match (true) { + str_starts_with($this->path, '//') => '/'.ltrim($this->path, '/'), + default => $this->path, + }; + } + + /** + * {@inheritDoc} + */ + public function getQuery(): ?string + { + return $this->query; + } + + /** + * {@inheritDoc} + */ + public function getFragment(): ?string + { + return $this->fragment; + } + + /** + * {@inheritDoc} + */ + public function withScheme(Stringable|string|null $scheme): UriInterface + { + $scheme = $this->formatScheme($this->filterString($scheme)); + + return match ($scheme) { + $this->scheme => $this, + default => new self( + $scheme, + $this->user, + $this->pass, + $this->host, + $this->port, + $this->path, + $this->query, + $this->fragment, + ), + }; + } + + /** + * Filter a string. + * + * @throws SyntaxError if the submitted data cannot be converted to string + */ + private function filterString(Stringable|string|null $str): ?string + { + $str = match (true) { + $str instanceof UriComponentInterface => $str->value(), + null === $str => null, + default => (string) $str, + }; + + return match (true) { + null === $str => null, + 1 === preg_match(self::REGEXP_INVALID_CHARS, $str) => throw new SyntaxError('The component `'.$str.'` contains invalid characters.'), + default => $str, + }; + } + + public function withUserInfo( + Stringable|string|null $user, + #[SensitiveParameter] Stringable|string|null $password = null + ): UriInterface { + $user = Encoder::encodeUser($this->filterString($user)); + $pass = Encoder::encodePassword($this->filterString($password)); + $userInfo = ('' !== $user) ? $this->formatUserInfo($user, $pass) : null; + + return match ($userInfo) { + $this->userInfo => $this, + default => new self( + $this->scheme, + $user, + $pass, + $this->host, + $this->port, + $this->path, + $this->query, + $this->fragment, + ), + }; + } + + public function withHost(Stringable|string|null $host): UriInterface + { + $host = $this->formatHost($this->filterString($host)); + + return match ($host) { + $this->host => $this, + default => new self( + $this->scheme, + $this->user, + $this->pass, + $host, + $this->port, + $this->path, + $this->query, + $this->fragment, + ), + }; + } + + public function withPort(int|null $port): UriInterface + { + $port = $this->formatPort($port); + + return match ($port) { + $this->port => $this, + default => new self( + $this->scheme, + $this->user, + $this->pass, + $this->host, + $port, + $this->path, + $this->query, + $this->fragment, + ), + }; + } + + public function withPath(Stringable|string $path): UriInterface + { + $path = $this->formatPath( + $this->filterString($path) ?? throw new SyntaxError('The path component cannot be null.') + ); + + return match ($path) { + $this->path => $this, + default => new self( + $this->scheme, + $this->user, + $this->pass, + $this->host, + $this->port, + $path, + $this->query, + $this->fragment, + ), + }; + } + + public function withQuery(Stringable|string|null $query): UriInterface + { + $query = Encoder::encodeQueryOrFragment($this->filterString($query)); + + return match ($query) { + $this->query => $this, + default => new self( + $this->scheme, + $this->user, + $this->pass, + $this->host, + $this->port, + $this->path, + $query, + $this->fragment, + ), + }; + } + + public function withFragment(Stringable|string|null $fragment): UriInterface + { + $fragment = Encoder::encodeQueryOrFragment($this->filterString($fragment)); + + return match ($fragment) { + $this->fragment => $this, + default => new self( + $this->scheme, + $this->user, + $this->pass, + $this->host, + $this->port, + $this->path, + $this->query, + $fragment, + ), + }; + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.5.0 + * @codeCoverageIgnore + * @see Uri::toComponents() + * + * @return ComponentMap + */ + #[Deprecated(message:'use League\Uri\Uri::toComponents() instead', since:'league/uri:7.5.0')] + public function getComponents(): array + { + return $this->toComponents(); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::new() + */ + #[Deprecated(message:'use League\Uri\Uri::new() instead', since:'league/uri:7.0.0')] + public static function createFromString(Stringable|string $uri = ''): self + { + return self::new($uri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::fromComponents() + * + * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result + */ + #[Deprecated(message:'use League\Uri\Uri::fromComponents() instead', since:'league/uri:7.0.0')] + public static function createFromComponents(array $components = []): self + { + return self::fromComponents($components); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @param resource|null $context + * + * @throws MissingFeature If ext/fileinfo is not installed + * @throws SyntaxError If the file does not exist or is not readable + * @see Uri::fromFileContents() + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + */ + #[Deprecated(message:'use League\Uri\Uri::fromDataPath() instead', since:'league/uri:7.0.0')] + public static function createFromDataPath(string $path, $context = null): self + { + return self::fromFileContents($path, $context); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::fromBaseUri() + * + * Creates a new instance from a URI and a Base URI. + * + * The returned URI must be absolute. + */ + #[Deprecated(message:'use League\Uri\Uri::fromBaseUri() instead', since:'league/uri:7.0.0')] + public static function createFromBaseUri( + Stringable|UriInterface|String $uri, + Stringable|UriInterface|String|null $baseUri = null + ): UriInterface { + return self::fromBaseUri($uri, $baseUri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::fromUnixPath() + * + * Create a new instance from a Unix path string. + */ + #[Deprecated(message:'use League\Uri\Uri::fromUnixPath() instead', since:'league/uri:7.0.0')] + public static function createFromUnixPath(string $uri = ''): self + { + return self::fromUnixPath($uri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::fromWindowsPath() + * + * Create a new instance from a local Windows path string. + */ + #[Deprecated(message:'use League\Uri\Uri::fromWindowsPath() instead', since:'league/uri:7.0.0')] + public static function createFromWindowsPath(string $uri = ''): self + { + return self::fromWindowsPath($uri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::new() + * + * Create a new instance from a URI object. + */ + #[Deprecated(message:'use League\Uri\Uri::new() instead', since:'league/uri:7.0.0')] + public static function createFromUri(Psr7UriInterface|UriInterface $uri): self + { + return self::new($uri); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Uri::fromServer() + * + * Create a new instance from the environment. + */ + #[Deprecated(message:'use League\Uri\Uri::fromServer() instead', since:'league/uri:7.0.0')] + public static function createFromServer(array $server): self + { + return self::fromServer($server); + } +} diff --git a/vendor/league/uri/UriInfo.php b/vendor/league/uri/UriInfo.php new file mode 100644 index 000000000..c782926b7 --- /dev/null +++ b/vendor/league/uri/UriInfo.php @@ -0,0 +1,105 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use Deprecated; +use League\Uri\Contracts\UriInterface; +use Psr\Http\Message\UriInterface as Psr7UriInterface; + +/** + * @deprecated since version 7.0.0 + * @codeCoverageIgnore + * @see BaseUri + */ +final class UriInfo +{ + /** + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Tells whether the URI represents an absolute URI. + */ + #[Deprecated(message:'use League\Uri\BaseUri::isAbsolute() instead', since:'league/uri:7.0.0')] + public static function isAbsolute(Psr7UriInterface|UriInterface $uri): bool + { + return BaseUri::from($uri)->isAbsolute(); + } + + /** + * Tell whether the URI represents a network path. + */ + #[Deprecated(message:'use League\Uri\BaseUri::isNetworkPath() instead', since:'league/uri:7.0.0')] + public static function isNetworkPath(Psr7UriInterface|UriInterface $uri): bool + { + return BaseUri::from($uri)->isNetworkPath(); + } + + /** + * Tells whether the URI represents an absolute path. + */ + #[Deprecated(message:'use League\Uri\BaseUri::isAbsolutePath() instead', since:'league/uri:7.0.0')] + public static function isAbsolutePath(Psr7UriInterface|UriInterface $uri): bool + { + return BaseUri::from($uri)->isAbsolutePath(); + } + + /** + * Tell whether the URI represents a relative path. + * + */ + #[Deprecated(message:'use League\Uri\BaseUri::isRelativePath() instead', since:'league/uri:7.0.0')] + public static function isRelativePath(Psr7UriInterface|UriInterface $uri): bool + { + return BaseUri::from($uri)->isRelativePath(); + } + + /** + * Tells whether both URI refers to the same document. + */ + #[Deprecated(message:'use League\Uri\BaseUri::isSameDocument() instead', since:'league/uri:7.0.0')] + public static function isSameDocument(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): bool + { + return BaseUri::from($baseUri)->isSameDocument($uri); + } + + /** + * Returns the URI origin property as defined by WHATWG URL living standard. + * + * {@see https://url.spec.whatwg.org/#origin} + * + * For URI without a special scheme the method returns null + * For URI with the file scheme the method will return null (as this is left to the implementation decision) + * For URI with a special scheme the method returns the scheme followed by its authority (without the userinfo part) + */ + #[Deprecated(message:'use League\Uri\BaseUri::origin() instead', since:'league/uri:7.0.0')] + public static function getOrigin(Psr7UriInterface|UriInterface $uri): ?string + { + return BaseUri::from($uri)->origin()?->__toString(); + } + + /** + * Tells whether two URI do not share the same origin. + * + * @see UriInfo::getOrigin() + */ + #[Deprecated(message:'use League\Uri\BaseUri::isCrossOrigin() instead', since:'league/uri:7.0.0')] + public static function isCrossOrigin(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): bool + { + return BaseUri::from($baseUri)->isCrossOrigin($uri); + } +} diff --git a/vendor/league/uri/UriResolver.php b/vendor/league/uri/UriResolver.php new file mode 100644 index 000000000..a50966ad8 --- /dev/null +++ b/vendor/league/uri/UriResolver.php @@ -0,0 +1,56 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use Deprecated; +use League\Uri\Contracts\UriInterface; +use Psr\Http\Message\UriInterface as Psr7UriInterface; + +/** + * @deprecated since version 7.0.0 + * @codeCoverageIgnore + * @see BaseUri + */ +final class UriResolver +{ + /** + * Resolves a URI against a base URI using RFC3986 rules. + * + * This method MUST retain the state of the submitted URI instance, and return + * a URI instance of the same type that contains the applied modifications. + * + * This method MUST be transparent when dealing with error and exceptions. + * It MUST not alter or silence them apart from validating its own parameters. + */ + #[Deprecated(message:'use League\Uri\BaseUri::resolve() instead', since:'league/uri:7.0.0')] + public static function resolve(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): Psr7UriInterface|UriInterface + { + return BaseUri::from($baseUri)->resolve($uri)->getUri(); + } + + /** + * Relativizes a URI according to a base URI. + * + * This method MUST retain the state of the submitted URI instance, and return + * a URI instance of the same type that contains the applied modifications. + * + * This method MUST be transparent when dealing with error and exceptions. + * It MUST not alter or silence them apart from validating its own parameters. + */ + #[Deprecated(message:'use League\Uri\BaseUri::relativize() instead', since:'league/uri:7.0.0')] + public static function relativize(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): Psr7UriInterface|UriInterface + { + return BaseUri::from($baseUri)->relativize($uri)->getUri(); + } +} diff --git a/vendor/league/uri/UriTemplate.php b/vendor/league/uri/UriTemplate.php new file mode 100644 index 000000000..882a7b51a --- /dev/null +++ b/vendor/league/uri/UriTemplate.php @@ -0,0 +1,123 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use League\Uri\Contracts\UriException; +use League\Uri\Contracts\UriInterface; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\UriTemplate\Template; +use League\Uri\UriTemplate\TemplateCanNotBeExpanded; +use League\Uri\UriTemplate\VariableBag; +use Stringable; + +use function array_fill_keys; +use function array_key_exists; + +/** + * Defines the URI Template syntax and the process for expanding a URI Template into a URI reference. + * + * @link https://tools.ietf.org/html/rfc6570 + * @package League\Uri + * @author Ignace Nyamagana Butera <nyamsprod@gmail.com> + * @since 6.1.0 + */ +final class UriTemplate +{ + private readonly Template $template; + private readonly VariableBag $defaultVariables; + + /** + * @throws SyntaxError if the template syntax is invalid + * @throws TemplateCanNotBeExpanded if the template or the variables are invalid + */ + public function __construct(Stringable|string $template, iterable $defaultVariables = []) + { + $this->template = $template instanceof Template ? $template : Template::new($template); + $this->defaultVariables = $this->filterVariables($defaultVariables); + } + + private function filterVariables(iterable $variables): VariableBag + { + if (!$variables instanceof VariableBag) { + $variables = new VariableBag($variables); + } + + return $variables + ->filter(fn ($value, string|int $name) => array_key_exists( + $name, + array_fill_keys($this->template->variableNames, 1) + )); + } + + public function getTemplate(): string + { + return $this->template->value; + } + + /** + * @return array<string> + */ + public function getVariableNames(): array + { + return $this->template->variableNames; + } + + public function getDefaultVariables(): array + { + return iterator_to_array($this->defaultVariables); + } + + /** + * Returns a new instance with the updated default variables. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the modified default variables. + * + * If present, variables whose name is not part of the current template + * possible variable names are removed. + * + * @throws TemplateCanNotBeExpanded if the variables are invalid + */ + public function withDefaultVariables(iterable $defaultVariables): self + { + $defaultVariables = $this->filterVariables($defaultVariables); + if ($defaultVariables == $this->defaultVariables) { + return $this; + } + + return new self($this->template, $defaultVariables); + } + + /** + * @throws TemplateCanNotBeExpanded if the variables are invalid + * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance + */ + public function expand(iterable $variables = []): UriInterface + { + return Uri::new($this->template->expand( + $this->filterVariables($variables)->replace($this->defaultVariables) + )); + } + + /** + * @throws TemplateCanNotBeExpanded if the variables are invalid or missing + * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance + */ + public function expandOrFail(iterable $variables = []): UriInterface + { + return Uri::new($this->template->expandOrFail( + $this->filterVariables($variables)->replace($this->defaultVariables) + )); + } +} diff --git a/vendor/league/uri/UriTemplate/Expression.php b/vendor/league/uri/UriTemplate/Expression.php new file mode 100644 index 000000000..d9d5b9054 --- /dev/null +++ b/vendor/league/uri/UriTemplate/Expression.php @@ -0,0 +1,99 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use Deprecated; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +use function array_filter; +use function array_map; +use function array_unique; +use function explode; +use function implode; + +/** + * @internal The class exposes the internal representation of an Expression and its usage + * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2 + */ +final class Expression +{ + /** @var array<VarSpecifier> */ + private readonly array $varSpecifiers; + /** @var array<string> */ + public readonly array $variableNames; + public readonly string $value; + + private function __construct(public readonly Operator $operator, VarSpecifier ...$varSpecifiers) + { + $this->varSpecifiers = $varSpecifiers; + $this->variableNames = array_unique( + array_map( + static fn (VarSpecifier $varSpecifier): string => $varSpecifier->name, + $varSpecifiers + ) + ); + $this->value = '{'.$operator->value.implode(',', array_map( + static fn (VarSpecifier $varSpecifier): string => $varSpecifier->toString(), + $varSpecifiers + )).'}'; + } + + /** + * @throws SyntaxError if the expression is invalid + */ + public static function new(Stringable|string $expression): self + { + $parts = Operator::parseExpression($expression); + + return new Expression($parts['operator'], ...array_map( + static fn (string $varSpec): VarSpecifier => VarSpecifier::new($varSpec), + explode(',', $parts['variables']) + )); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @throws SyntaxError if the expression is invalid + * @see Expression::new() + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + */ + #[Deprecated(message:'use League\Uri\UriTemplate\Exppression::new() instead', since:'league/uri:7.0.0')] + public static function createFromString(Stringable|string $expression): self + { + return self::new($expression); + } + + public function expand(VariableBag $variables): string + { + $expanded = implode( + $this->operator->separator(), + array_filter( + array_map( + fn (VarSpecifier $varSpecifier): string => $this->operator->expand($varSpecifier, $variables), + $this->varSpecifiers + ), + static fn ($value): bool => '' !== $value + ) + ); + + return match ('') { + $expanded => '', + default => $this->operator->first().$expanded, + }; + } +} diff --git a/vendor/league/uri/UriTemplate/Operator.php b/vendor/league/uri/UriTemplate/Operator.php new file mode 100644 index 000000000..0316b80d8 --- /dev/null +++ b/vendor/league/uri/UriTemplate/Operator.php @@ -0,0 +1,225 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use League\Uri\Encoder; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +use function implode; +use function is_array; +use function preg_match; +use function rawurlencode; +use function str_contains; +use function substr; + +/** + * Processing behavior according to the expression type operator. + * + * @internal The class exposes the internal representation of an Operator and its usage + * + * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2 + * @link https://tools.ietf.org/html/rfc6570#appendix-A + */ +enum Operator: string +{ + /** + * Expression regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc6570#section-2.2 + */ + private const REGEXP_EXPRESSION = '/^\{(?:(?<operator>[\.\/;\?&\=,\!@\|\+#])?(?<variables>[^\}]*))\}$/'; + + /** + * Reserved Operator characters. + * + * @link https://tools.ietf.org/html/rfc6570#section-2.2 + */ + private const RESERVED_OPERATOR = '=,!@|'; + + case None = ''; + case ReservedChars = '+'; + case Label = '.'; + case Path = '/'; + case PathParam = ';'; + case Query = '?'; + case QueryPair = '&'; + case Fragment = '#'; + + public function first(): string + { + return match ($this) { + self::None, self::ReservedChars => '', + default => $this->value, + }; + } + + public function separator(): string + { + return match ($this) { + self::None, self::ReservedChars, self::Fragment => ',', + self::Query, self::QueryPair => '&', + default => $this->value, + }; + } + + public function isNamed(): bool + { + return match ($this) { + self::Query, self::PathParam, self::QueryPair => true, + default => false, + }; + } + + /** + * Removes percent encoding on reserved characters (used with + and # modifiers). + */ + public function decode(string $var): string + { + return match ($this) { + Operator::ReservedChars, Operator::Fragment => (string) Encoder::encodeQueryOrFragment($var), + default => rawurlencode($var), + }; + } + + /** + * @throws SyntaxError if the expression is invalid + * @throws SyntaxError if the operator used in the expression is invalid + * @throws SyntaxError if the contained variable specifiers are invalid + * + * @return array{operator:Operator, variables:string} + */ + public static function parseExpression(Stringable|string $expression): array + { + $expression = (string) $expression; + if (1 !== preg_match(self::REGEXP_EXPRESSION, $expression, $parts)) { + throw new SyntaxError('The expression "'.$expression.'" is invalid.'); + } + + /** @var array{operator:string, variables:string} $parts */ + $parts = $parts + ['operator' => '']; + if ('' !== $parts['operator'] && str_contains(self::RESERVED_OPERATOR, $parts['operator'])) { + throw new SyntaxError('The operator used in the expression "'.$expression.'" is reserved.'); + } + + return [ + 'operator' => self::from($parts['operator']), + 'variables' => $parts['variables'], + ]; + } + + /** + * Replaces an expression with the given variables. + * + * @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied + * @throws TemplateCanNotBeExpanded if the variables contains nested array values + */ + public function expand(VarSpecifier $varSpecifier, VariableBag $variables): string + { + $value = $variables->fetch($varSpecifier->name); + if (null === $value) { + return ''; + } + + [$expanded, $actualQuery] = $this->inject($value, $varSpecifier); + if (!$actualQuery) { + return $expanded; + } + + if ('&' !== $this->separator() && '' === $expanded) { + return $varSpecifier->name; + } + + return $varSpecifier->name.'='.$expanded; + } + + /** + * @param string|array<string> $value + * + * @return array{0:string, 1:bool} + */ + private function inject(array|string $value, VarSpecifier $varSpec): array + { + if (is_array($value)) { + return $this->replaceList($value, $varSpec); + } + + if (':' === $varSpec->modifier) { + $value = substr($value, 0, $varSpec->position); + } + + return [$this->decode($value), $this->isNamed()]; + } + + /** + * Expands an expression using a list of values. + * + * @param array<string> $value + * + * @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied + * + * @return array{0:string, 1:bool} + */ + private function replaceList(array $value, VarSpecifier $varSpec): array + { + if (':' === $varSpec->modifier) { + throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name); + } + + if ([] === $value) { + return ['', false]; + } + + $pairs = []; + $isList = array_is_list($value); + $useQuery = $this->isNamed(); + foreach ($value as $key => $var) { + if (!$isList) { + $key = rawurlencode((string) $key); + } + + $var = $this->decode($var); + if ('*' === $varSpec->modifier) { + if (!$isList) { + $var = $key.'='.$var; + } elseif ($key > 0 && $useQuery) { + $var = $varSpec->name.'='.$var; + } + } + + $pairs[$key] = $var; + } + + if ('*' === $varSpec->modifier) { + if (!$isList) { + // Don't prepend the value name when using the `explode` modifier with an associative array. + $useQuery = false; + } + + return [implode($this->separator(), $pairs), $useQuery]; + } + + if (!$isList) { + // When an associative array is encountered and the `explode` modifier is not set, then + // the result must be a comma separated list of keys followed by their respective values. + $retVal = []; + foreach ($pairs as $offset => $data) { + $retVal[$offset] = $offset.','.$data; + } + $pairs = $retVal; + } + + return [implode(',', $pairs), $useQuery]; + } +} diff --git a/vendor/league/uri/UriTemplate/Template.php b/vendor/league/uri/UriTemplate/Template.php new file mode 100644 index 000000000..2727c9480 --- /dev/null +++ b/vendor/league/uri/UriTemplate/Template.php @@ -0,0 +1,145 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use Deprecated; +use League\Uri\Exceptions\SyntaxError; +use Stringable; + +use function array_filter; +use function array_map; +use function array_reduce; +use function array_unique; +use function preg_match_all; +use function preg_replace; +use function str_contains; +use function str_replace; + +use const PREG_SET_ORDER; + +/** + * @internal The class exposes the internal representation of a Template and its usage + */ +final class Template implements Stringable +{ + /** + * Expression regular expression pattern. + */ + private const REGEXP_EXPRESSION_DETECTOR = '/(?<expression>\{[^}]*})/x'; + + /** @var array<Expression> */ + private readonly array $expressions; + /** @var array<string> */ + public readonly array $variableNames; + + private function __construct(public readonly string $value, Expression ...$expressions) + { + $this->expressions = $expressions; + $this->variableNames = array_unique( + array_merge( + ...array_map( + static fn (Expression $expression): array => $expression->variableNames, + $expressions + ) + ) + ); + } + + /** + * @throws SyntaxError if the template contains invalid expressions + * @throws SyntaxError if the template contains invalid variable specification + */ + public static function new(Stringable|string $template): self + { + $template = (string) $template; + /** @var string $remainder */ + $remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template); + if (str_contains($remainder, '{') || str_contains($remainder, '}')) { + throw new SyntaxError('The template "'.$template.'" contains invalid expressions.'); + } + + preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER); + + return new self($template, ...array_values( + array_reduce($founds, function (array $carry, array $found): array { + if (!isset($carry[$found['expression']])) { + $carry[$found['expression']] = Expression::new($found['expression']); + } + + return $carry; + }, []) + )); + } + + /** + * @throws TemplateCanNotBeExpanded if the variables are invalid + */ + public function expand(iterable $variables = []): string + { + if (!$variables instanceof VariableBag) { + $variables = new VariableBag($variables); + } + + return $this->expandAll($variables); + } + + /** + * @throws TemplateCanNotBeExpanded if the variables are invalid or missing + */ + public function expandOrFail(iterable $variables = []): string + { + if (!$variables instanceof VariableBag) { + $variables = new VariableBag($variables); + } + + $missing = array_filter($this->variableNames, fn (string $name): bool => !isset($variables[$name])); + if ([] !== $missing) { + throw TemplateCanNotBeExpanded::dueToMissingVariables(...$missing); + } + + return $this->expandAll($variables); + } + + private function expandAll(VariableBag $variables): string + { + return array_reduce( + $this->expressions, + fn (string $uri, Expression $expr): string => str_replace($expr->value, $expr->expand($variables), $uri), + $this->value + ); + } + + public function __toString(): string + { + return $this->value; + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @throws SyntaxError if the template contains invalid expressions + * @throws SyntaxError if the template contains invalid variable specification + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + * @see Template::new() + * + * Create a new instance from a string. + * + */ + #[Deprecated(message:'use League\Uri\UriTemplate\Template::new() instead', since:'league/uri:7.0.0')] + public static function createFromString(Stringable|string $template): self + { + return self::new($template); + } +} diff --git a/vendor/league/uri/UriTemplate/TemplateCanNotBeExpanded.php b/vendor/league/uri/UriTemplate/TemplateCanNotBeExpanded.php new file mode 100644 index 000000000..70e1601d1 --- /dev/null +++ b/vendor/league/uri/UriTemplate/TemplateCanNotBeExpanded.php @@ -0,0 +1,44 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use InvalidArgumentException; +use League\Uri\Contracts\UriException; + +class TemplateCanNotBeExpanded extends InvalidArgumentException implements UriException +{ + public readonly array $variablesNames; + + public function __construct(string $message = '', string ...$variableNames) + { + parent::__construct($message, 0, null); + + $this->variablesNames = $variableNames; + } + + public static function dueToUnableToProcessValueListWithPrefix(string $variableName): self + { + return new self('The ":" modifier cannot be applied on "'.$variableName.'" since it is a list of values.', $variableName); + } + + public static function dueToNestedListOfValue(string $variableName): self + { + return new self('The "'.$variableName.'" cannot be a nested list.', $variableName); + } + + public static function dueToMissingVariables(string ...$variableNames): self + { + return new self('The following required variables are missing: `'.implode('`, `', $variableNames).'`.', ...$variableNames); + } +} diff --git a/vendor/league/uri/UriTemplate/VarSpecifier.php b/vendor/league/uri/UriTemplate/VarSpecifier.php new file mode 100644 index 000000000..1730d3ca1 --- /dev/null +++ b/vendor/league/uri/UriTemplate/VarSpecifier.php @@ -0,0 +1,73 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use League\Uri\Exceptions\SyntaxError; + +use function preg_match; + +/** + * @internal The class exposes the internal representation of a Var Specifier + * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.3 + */ +final class VarSpecifier +{ + /** + * Variables specification regular expression pattern. + * + * @link https://tools.ietf.org/html/rfc6570#section-2.3 + */ + private const REGEXP_VARSPEC = '/^(?<name>(?:[A-z0-9_\.]|%[0-9a-fA-F]{2})+)(?<modifier>\:(?<position>\d+)|\*)?$/'; + + private const MODIFIER_POSITION_MAX_POSITION = 10_000; + + private function __construct( + public readonly string $name, + public readonly string $modifier, + public readonly int $position + ) { + } + + public static function new(string $specification): self + { + if (1 !== preg_match(self::REGEXP_VARSPEC, $specification, $parsed)) { + throw new SyntaxError('The variable specification "'.$specification.'" is invalid.'); + } + + $properties = ['name' => $parsed['name'], 'modifier' => $parsed['modifier'] ?? '', 'position' => $parsed['position'] ?? '']; + + if ('' !== $properties['position']) { + $properties['position'] = (int) $properties['position']; + $properties['modifier'] = ':'; + } + + if ('' === $properties['position']) { + $properties['position'] = 0; + } + + if (self::MODIFIER_POSITION_MAX_POSITION <= $properties['position']) { + throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be lower than 10000.'); + } + + return new self($properties['name'], $properties['modifier'], $properties['position']); + } + + public function toString(): string + { + return $this->name.$this->modifier.match (true) { + 0 < $this->position => $this->position, + default => '', + }; + } +} diff --git a/vendor/league/uri/UriTemplate/VariableBag.php b/vendor/league/uri/UriTemplate/VariableBag.php new file mode 100644 index 000000000..cf6d08f30 --- /dev/null +++ b/vendor/league/uri/UriTemplate/VariableBag.php @@ -0,0 +1,151 @@ +<?php + +/** + * League.Uri (https://uri.thephpleague.com) + * + * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use ArrayAccess; +use Closure; +use Countable; +use IteratorAggregate; +use Stringable; +use Traversable; + +use function array_filter; +use function is_bool; +use function is_scalar; + +use const ARRAY_FILTER_USE_BOTH; + +/** + * @internal The class exposes the internal representation of variable bags + * + * @phpstan-type InputValue string|bool|int|float|array<string|bool|int|float> + * + * @implements ArrayAccess<string, InputValue> + * @implements IteratorAggregate<string, InputValue> + */ +final class VariableBag implements ArrayAccess, Countable, IteratorAggregate +{ + /** + * @var array<string,string|array<string>> + */ + private array $variables = []; + + /** + * @param iterable<array-key, InputValue> $variables + */ + public function __construct(iterable $variables = []) + { + foreach ($variables as $name => $value) { + $this->assign((string) $name, $value); + } + } + + public function count(): int + { + return count($this->variables); + } + + public function getIterator(): Traversable + { + yield from $this->variables; + } + + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->variables); + } + + public function offsetUnset(mixed $offset): void + { + unset($this->variables[$offset]); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->assign($offset, $value); /* @phpstan-ignore-line */ + } + + public function offsetGet(mixed $offset): mixed + { + return $this->fetch($offset); + } + + /** + * Tells whether the bag is empty or not. + */ + public function isEmpty(): bool + { + return [] === $this->variables; + } + + /** + * Tells whether the bag is empty or not. + */ + public function isNotEmpty(): bool + { + return [] !== $this->variables; + } + + /** + * Fetches the variable value if none found returns null. + * + * @return null|string|array<string> + */ + public function fetch(string $name): null|string|array + { + return $this->variables[$name] ?? null; + } + + /** + * @param Stringable|InputValue $value + */ + public function assign(string $name, Stringable|string|bool|int|float|array|null $value): void + { + $this->variables[$name] = $this->normalizeValue($value, $name, true); + } + + /** + * @param Stringable|InputValue $value + * + * @throws TemplateCanNotBeExpanded if the value contains nested list + */ + private function normalizeValue( + Stringable|string|float|int|bool|array|null $value, + string $name, + bool $isNestedListAllowed + ): array|string { + return match (true) { + is_bool($value) => true === $value ? '1' : '0', + (null === $value || is_scalar($value) || $value instanceof Stringable) => (string) $value, + !$isNestedListAllowed => throw TemplateCanNotBeExpanded::dueToNestedListOfValue($name), + default => array_map(fn ($var): array|string => self::normalizeValue($var, $name, false), $value), + }; + } + + /** + * Replaces elements from passed variables into the current instance. + */ + public function replace(VariableBag $variables): self + { + return new self($this->variables + $variables->variables); + } + + /** + * Filters elements using the closure. + */ + public function filter(Closure $fn): self + { + return new self(array_filter($this->variables, $fn, ARRAY_FILTER_USE_BOTH)); + } +} diff --git a/vendor/league/uri/composer.json b/vendor/league/uri/composer.json new file mode 100644 index 000000000..942c73e70 --- /dev/null +++ b/vendor/league/uri/composer.json @@ -0,0 +1,76 @@ +{ + "name": "league/uri", + "type": "library", + "description" : "URI manipulation library", + "keywords": [ + "url", + "uri", + "rfc3986", + "rfc3987", + "rfc6570", + "psr-7", + "parse_url", + "http", + "https", + "ws", + "ftp", + "data-uri", + "file-uri", + "middleware", + "parse_str", + "query-string", + "querystring", + "hostname", + "uri-template" + ], + "license": "MIT", + "homepage": "https://uri.thephpleague.com", + "authors": [ + { + "name" : "Ignace Nyamagana Butera", + "email" : "nyamsprod@gmail.com", + "homepage" : "https://nyamsprod.com" + } + ], + "support": { + "forum": "https://thephpleague.slack.com", + "docs": "https://uri.thephpleague.com", + "issues": "https://github.com/thephpleague/uri-src/issues" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nyamsprod" + } + ], + "require": { + "php": "^8.1", + "league/uri-interfaces": "^7.5" + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components" : "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "config": { + "sort-packages": true + } +} |