Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill

- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er)
- Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link
- Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest
- Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen)
- Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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;
interface Conditionable
{
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable(static): bool)|bool $condition
* @param callable(static): (static|null) $onSuccess
* @param ?callable(static): (static|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,125 @@
<?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 BackedEnum;
use Countable;
use Iterator;
use IteratorAggregate;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
/**
* @extends IteratorAggregate<int, string>
*
* @method bool isSubdomainOf(BackedEnum|Stringable|string|null $parentHost) Tells whether the current domain instance is a subdomain of the parent host.
* @method bool hasSubdomain(BackedEnum|Stringable|string|null $childHost) Tells whether the submitted host is a subdomain of the current instance.
* @method bool isSiblingOf(BackedEnum|Stringable|string|null $siblingHost) Tells whether the submitted host share the same parent domain as the current instance.
* @method static commonAncestorWith(BackedEnum|Stringable|string|null $other) Returns the common longest ancestor between 2 domain. The returned domain is empty if no ancestor is found
* @method static parentHost() Returns the current parent domain for the current instance. The returned domain is empty if no ancestor is found
* @method bool isEmpty() Tells whether the domain contains any label.
*/
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;
}

View File

@@ -0,0 +1,55 @@
<?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;
/**
* @see https://wicg.github.io/scroll-to-text-fragment/#the-fragment-directive
*
* @method string toFragmentValue() returns the encoded string representation of the directive as a fragment string
*/
interface FragmentDirective extends Stringable
{
/**
* The decoded Directive name.
*
* @return non-empty-string
*/
public function name(): string;
/**
* The decoded Directive value.
*/
public function value(): ?string;
/**
* The encoded string representation of the directive.
*/
public function toString(): string;
/**
* The encoded string representation of the fragment using
* the Stringable interface.
*
* @see FragmentDirective::toString()
*/
public function __toString(): string;
/**
* Tells whether the submitted value is equals to the string
* representation of the given directive.
*/
public function equals(mixed $directive): bool;
}

View File

@@ -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\Contracts;
/**
* @method self normalize() returns the normalized string representation of the component
*/
interface FragmentInterface extends UriComponentInterface
{
/**
* Returns the decoded fragment.
*/
public function decoded(): ?string;
}

View File

@@ -0,0 +1,59 @@
<?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;
/**
* @method string|null encoded() returns RFC3986 encoded host
*/
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;
}

View File

@@ -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;
}

View File

@@ -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\SyntaxError;
/**
* @method static normalize() returns the normalized string representation of the component
*/
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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,272 @@
<?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 BackedEnum;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
use League\Uri\QueryComposeMode;
use League\Uri\StringCoercionMode;
use Stringable;
/**
* @extends IteratorAggregate<array{0:string, 1:string|null}>
*
* @method string|null toFormData() Returns the string representation using the application/www-form-urlencoded rules
* @method string|null toRFC3986() Returns the string representation using RFC3986 rules
* @method string|null first(string $key) Returns the first value associated with the given name
* @method string|null last(string $key) Returns the first value associated with the given name
* @method int|null indexOf(string $key, int $nth = 0) Returns the offset of the pair based on its key and its nth occurrence; negative occurrences are supported
* @method int|null indexOfValue(?string $value, int $nth = 0) Returns the offset of the pair based on its value and its nth occurrence; negative occurrences are supported
* @method array pair(int $offset) Returns the key/value pair at the given numeric offset; negative occurrences are supported
* @method int countDistinctKeys() Returns the total number of distinct keys
* @method string|null valueAt(int $offset): Returns the value at the given numeric offset; negative occurrences are supported
* @method string keyAt(int $offset): Returns the key at the given numeric offset; negative occurrences are supported
* @method self normalize() returns the normalized string representation of the component
* @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys.
* @method self withoutPairByValue(array|BackedEnum|Stringable|string|int|bool|null $values, StringCoercionMode $coercionMode = StringCoercionMode::Native) Returns an instance without pairs with the specified values.
* @method self withoutPairByKeyValue(string $key, BackedEnum|Stringable|string|int|bool|null $value, StringCoercionMode $coercionMode = StringCoercionMode::Native) 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 array getList(string $name) Returns the list associated with the given name or an empty array if it does not exist.
* @method bool hasList(string ...$names) Tells whether the parameter list exists in the query.
* @method self appendList(string $name, array $values, QueryComposeMode $composeMode = QueryComposeMode::Native) Appends a parameter to the query string
* @method self withList(string $name, array $values, QueryComposeMode $composeMode = QueryComposeMode::Native) Adds a new parameter to the query string and remove any previously set values
* @method self withoutList(string ...$names) Removes any given list associated with the given names
* @method self withoutLists() Removes all lists from the query string
* @method self onlyLists() Removes all pairs without a valid PHP's bracket notation
*/
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;
}

View File

@@ -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 $path): 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 $path): 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;
}

View File

@@ -0,0 +1,27 @@
<?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 Transformable
{
/**
* Apply a transformation to this instance and return a new instance.
*
* This method MUST retain the state of the current instance, and return
* a new instance of the same type.
*
* @param callable(static): static $callback
*/
public function transform(callable $callback): static;
}

View File

@@ -0,0 +1,29 @@
<?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;
/**
* @deprecated since version 7.6.0
*/
interface UriAccess
{
public function getUri(): UriInterface|Psr7UriInterface;
/**
* Returns the RFC3986 string representation of the complete URI.
*/
public function getUriString(): string;
}

View File

@@ -0,0 +1,79 @@
<?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;
/**
* @method static when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null) conditionally return a new instance
* @method bool equals(mixed $value) tells whether the submitted value is equal to the current instance value
*/
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;
}

View File

@@ -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
{
}

View File

@@ -0,0 +1,321 @@
<?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 self withUsername(?string $user) returns a new URI instance with the user component updated.
* @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource.
* @method self withPassword(?string $password) returns a new URI instance with the password component updated.
* @method string toAsciiString() returns the string representation of the URI in its RFC3986 form
* @method string toUnicodeString() returns the string representation of the URI in its RFC3987 form (the host is in its IDN form)
* @method array toComponents() returns an associative array containing all the URI components.
* @method self normalize() returns a new URI instance with normalized components
* @method self resolve(UriInterface $uri) resolves a URI against a base URI using RFC3986 rules
* @method self relativize(UriInterface $uri) relativize a URI against a base URI using RFC3986 rules
*/
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;
}

View File

@@ -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;
}

506
vendor/league/uri-interfaces/Encoder.php vendored Executable file
View File

@@ -0,0 +1,506 @@
<?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 BackedEnum;
use Closure;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\IPv6\Converter as IPv6Converter;
use SensitiveParameter;
use Stringable;
use Throwable;
use function explode;
use function filter_var;
use function gettype;
use function in_array;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function str_starts_with;
use function strtolower;
use function strtoupper;
use const FILTER_FLAG_IPV4;
use const FILTER_VALIDATE_IP;
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})';
/**
* Unreserved characters.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3
*/
private const REGEXP_UNRESERVED_CHARACTERS = ',%(2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[AaFf]|6[1-9A-Fa-f]|7[0-9A-Ea-e]),';
/**
* Tell whether the user component is correctly encoded.
*/
public static function isUserEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode User.
*
* All generic delimiters MUST be encoded
*/
public static function encodeUser(BackedEnum|Stringable|string|null $user): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($user, $pattern);
}
/**
* Normalize user component.
*
* 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.
*/
public static function normalizeUser(BackedEnum|Stringable|string|null $user): ?string
{
return self::normalize(self::encodeUser(self::decodeUnreservedCharacters($user)));
}
private static function normalize(?string $component): ?string
{
if (null === $component) {
return null;
}
return (string) preg_replace_callback(
'/%[0-9a-f]{2}/i',
static fn (array $found) => strtoupper($found[0]),
$component
);
}
/**
* Tell whether the password component is correctly encoded.
*/
public static function isPasswordEncoded(#[SensitiveParameter] BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode Password.
*
* Generic delimiters ":" MUST NOT be encoded
*/
public static function encodePassword(#[SensitiveParameter] BackedEnum|Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
/**
* Normalize password component.
*
* 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.
*/
public static function normalizePassword(#[SensitiveParameter] BackedEnum|Stringable|string|null $password): ?string
{
return self::normalize(self::encodePassword(self::decodeUnreservedCharacters($password)));
}
/**
* Tell whether the userInfo component is correctly encoded.
*/
public static function isUserInfoEncoded(#[SensitiveParameter] BackedEnum|Stringable|string|null $userInfo): bool
{
if (null === $userInfo) {
return true;
}
if ($userInfo instanceof BackedEnum) {
$userInfo = $userInfo->value;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
return self::isUserEncoded($user)
&& self::isPasswordEncoded($password);
}
public static function encodeUserInfo(#[SensitiveParameter] BackedEnum|Stringable|string|null $userInfo): ?string
{
if (null === $userInfo) {
return null;
}
if ($userInfo instanceof BackedEnum) {
$userInfo = $userInfo->value;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
$userInfo = self::encodeUser($user);
if (null === $password) {
return $userInfo;
}
return $userInfo.':'.self::encodePassword($password);
}
public static function normalizeUserInfo(#[SensitiveParameter] BackedEnum|Stringable|string|null $userInfo): ?string
{
if (null === $userInfo) {
return null;
}
if ($userInfo instanceof BackedEnum) {
$userInfo = $userInfo->value;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
$userInfo = self::normalizeUser($user);
if (null === $password) {
return $userInfo;
}
return $userInfo.':'.self::normalizePassword($password);
}
/**
* Decodes all the URI component characters.
*/
public static function decodeAll(BackedEnum|Stringable|string|null $component): ?string
{
return self::decode($component, static fn (array $matches): string => rawurldecode($matches[0]));
}
/**
* Decodes the URI component without decoding the unreserved characters which are already encoded.
*/
public static function decodeNecessary(BackedEnum|Stringable|string|int|null $component): ?string
{
$decoder = static function (array $matches): string {
if (1 === preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0])) {
return strtoupper($matches[0]);
}
return rawurldecode($matches[0]);
};
return self::decode($component, $decoder);
}
/**
* Decodes the component unreserved characters.
*/
public static function decodeUnreservedCharacters(BackedEnum|Stringable|string|null $str): ?string
{
if ($str instanceof BackedEnum) {
$str = $str->value;
}
if (null === $str) {
return null;
}
return preg_replace_callback(
self::REGEXP_UNRESERVED_CHARACTERS,
static fn (array $matches): string => rawurldecode($matches[0]),
(string) $str
);
}
/**
* Tell whether the path component is correctly encoded.
*/
public static function isPathEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode Path.
*
* Generic delimiters ":", "@", and "/" MUST NOT be encoded
*/
public static function encodePath(BackedEnum|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);
}
/**
* Decodes the path component while preserving characters that should not be decoded in the context of a full valid URI.
*/
public static function decodePath(BackedEnum|Stringable|string|null $path): ?string
{
$decoder = static function (array $matches): string {
$encodedChar = strtoupper($matches[0]);
return in_array($encodedChar, ['%2F', '%20', '%3F', '%23'], true) ? $encodedChar : rawurldecode($encodedChar);
};
return self::decode($path, $decoder);
}
/**
* Normalize path component.
*
* 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.
*/
public static function normalizePath(BackedEnum|Stringable|string|null $component): ?string
{
return self::normalize(self::encodePath(self::decodePath($component)));
}
/**
* Tell whether the query component is correctly encoded.
*/
public static function isQueryEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.'\/?%]+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Decodes the query component while preserving characters that should not be decoded in the context of a full valid URI.
*/
public static function decodeQuery(BackedEnum|Stringable|string|null $path): ?string
{
$decoder = static function (array $matches): string {
$encodedChar = strtoupper($matches[0]);
return in_array($encodedChar, ['%26', '%3D', '%20', '%23', '%3F'], true) ? $encodedChar : rawurldecode($encodedChar);
};
return self::decode($path, $decoder);
}
/**
* Normalize the query component.
*
* 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.
*/
public static function normalizeQuery(BackedEnum|Stringable|string|null $query): ?string
{
return self::normalize(self::encodeQueryOrFragment(self::decodeQuery($query)));
}
/**
* Tell whether the query component is correctly encoded.
*/
public static function isFragmentEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?%]|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Decodes the fragment component while preserving characters that should not be decoded in the context of a full valid URI.
*/
public static function decodeFragment(BackedEnum|Stringable|string|null $path): ?string
{
return self::decode($path, static fn (array $matches): string => '%20' === $matches[0] ? $matches[0] : rawurldecode($matches[0]));
}
/**
* Normalize the fragment component.
*
* 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.
*/
public static function normalizeFragment(BackedEnum|Stringable|string|null $fragment): ?string
{
return self::normalize(self::encodeQueryOrFragment(self::decodeFragment($fragment)));
}
/**
* Normalize the host component.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2
*
* 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.
*/
public static function normalizeHost(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = (string) $host->value;
}
if ($host instanceof Stringable) {
$host = (string) $host;
}
if (null === $host || '' === $host || false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $host;
}
if (str_starts_with($host, '[')) {
return IPv6Converter::normalize($host);
}
$host = strtolower($host);
return (!str_contains($host, '%')) ? $host : preg_replace_callback(
'/%[a-f0-9]{2}/',
fn (array $matches) => 1 === preg_match('/%([0-7][0-9a-f])/', $matches[0]) ? rawurldecode($matches[0]) : strtoupper($matches[0]),
$host
);
}
/**
* Encode Query or Fragment.
*
* Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded
*/
public static function encodeQueryOrFragment(BackedEnum|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.'/';
$encoder = static fn (array $found): string => 1 === preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($found[0])) ? rawurlencode($found[0]) : $found[0];
$filteredComponent = self::filterComponent($component);
return match (true) {
null === $filteredComponent => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))),
1 === preg_match(self::REGEXP_CHARS_INVALID, $filteredComponent) => rawurlencode($filteredComponent),
default => (string) preg_replace_callback($pattern, $encoder, $filteredComponent),
};
}
private static function filterComponent(mixed $component): ?string
{
try {
return StringCoercionMode::Native->coerce($component);
} catch (Throwable $exception) {
throw new SyntaxError(
sprintf('The component must be a scalar value `%s` given.', gettype($component)),
previous: $exception
);
}
}
/**
* Encodes the URI component characters using a regular expression to find which characters need encoding.
*/
private static function encode(BackedEnum|Stringable|string|int|bool|null $component, string $pattern): ?string
{
$component = self::filterComponent($component);
if (null === $component || '' === $component) {
return $component;
}
return (string) preg_replace_callback(
$pattern,
static fn (array $found): string => 1 === preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($found[0])) ? rawurlencode($found[0]) : $found[0],
$component
);
}
/**
* Decodes the URI component characters using a closure.
*/
private static function decode(BackedEnum|Stringable|string|int|null $component, Closure $decoder): ?string
{
$component = self::filterComponent($component);
if (null === $component || '' === $component) {
return $component;
}
if (1 === preg_match(self::REGEXP_CHARS_INVALID, $component)) {
throw new SyntaxError('Invalid component string: '.$component.'.');
}
if (1 === preg_match(self::REGEXP_CHARS_ENCODED, $component)) {
return (string) preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decoder, $component);
}
return $component;
}
/**
* Decodes the URI component without decoding the unreserved characters which are already encoded.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see Encoder::decodeNecessary()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\Encoder::decodeNecessary() instead', since:'league/uri:7.6.0')]
public static function decodePartial(BackedEnum|Stringable|string|int|null $component): ?string
{
return self::decodeNecessary($component);
}
}

View File

@@ -0,0 +1,51 @@
<?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 BackedEnum;
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(BackedEnum|Stringable|string $host, Result $result): self
{
$reasons = array_map(fn (Error $error): string => $error->description(), $result->errors());
if ($host instanceof BackedEnum) {
$host = (string) $host->value;
}
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;
}
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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;
use finfo;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\IPv4\Calculator;
use function class_exists;
use function defined;
use function extension_loaded;
use function function_exists;
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);
$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'));
$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));
$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 implementation.');
}
public static function supportsDom(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? extension_loaded('dom');
$isSupported || throw new MissingFeature('To use a DOM related feature, the DOM extension must be installed in your system.');
}
}

20
vendor/league/uri-interfaces/HostFormat.php vendored Executable file
View File

@@ -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;
enum HostFormat
{
case Ascii;
case Unicode;
}

446
vendor/league/uri-interfaces/HostRecord.php vendored Executable file
View File

@@ -0,0 +1,446 @@
<?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 BackedEnum;
use Exception;
use JsonSerializable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use Stringable;
use Throwable;
use function array_key_first;
use function count;
use function explode;
use function filter_var;
use function get_object_vars;
use function in_array;
use function inet_pton;
use function is_object;
use function preg_match;
use function rawurldecode;
use function strpos;
use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* @phpstan-type HostRecordSerializedShape array{0: array{host: ?string}, 1: array{}}
*/
final class HostRecord implements JsonSerializable
{
/**
* Maximum number of host cached.
*
* @var int
*/
private const MAXIMUM_HOST_CACHED = 100;
private const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* invalid characters in host regular expression
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* 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))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Domain name regular expression.
*
* Everything but the domain name length is validated
*
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://regex101.com/r/71j6rt/1
*/
private const REGEXP_DOMAIN_NAME = '/
(?(DEFINE)
(?<let_dig> [a-z0-9]) # alpha digit
(?<let_dig_hyp> [a-z0-9-]) # alpha digit and hyphen
(?<ldh_str> (?&let_dig_hyp){0,61}(?&let_dig)) # domain label end
(?<label> (?&let_dig)((?&ldh_str))?) # domain label
(?<domain> (?&label)(\.(?&label)){0,126}\.?) # domain name
)
^(?&domain)$
/ix';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* IPvFuture regular expression
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F\d])+\.
(?:
(?<unreserved>[a-z\d_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@ ]/';
private const ADDRESS_BLOCK = "\xfe\x80";
private ?bool $isDomainName = null;
private ?bool $hasZoneIdentifier = null;
private bool $asciiIsLoaded = false;
private ?string $hostAsAscii = null;
private bool $unicodeIsLoaded = false;
private ?string $hostAsUnicode = null;
private bool $isIpVersionLoaded = false;
private ?string $ipVersion = null;
private bool $isIpValueLoaded = false;
private ?string $ipValue = null;
private function __construct(
public readonly ?string $value,
public readonly HostType $type,
public readonly HostFormat $format
) {
}
public function hasZoneIdentifier(): bool
{
return $this->hasZoneIdentifier ??= HostType::Ipv6 === $this->type && str_contains((string) $this->value, '%');
}
public function toAscii(): ?string
{
if (!$this->asciiIsLoaded) {
$this->asciiIsLoaded = true;
$this->hostAsAscii = (function (): ?string {
if (HostType::RegisteredName !== $this->type || null === $this->value) {
return $this->value;
}
$formattedHost = rawurldecode($this->value);
if ($formattedHost === $this->value) {
return $this->isDomainType() ? IdnConverter::toAscii($this->value)->domain() : strtolower($formattedHost);
}
return Encoder::normalizeHost($this->value);
})();
}
return $this->hostAsAscii;
}
public function toUnicode(): ?string
{
if (!$this->unicodeIsLoaded) {
$this->unicodeIsLoaded = true;
$this->hostAsUnicode = $this->isDomainType() && null !== $this->value ? IdnConverter::toUnicode($this->value)->domain() : $this->value;
}
return $this->hostAsUnicode;
}
public function isDomainType(): bool
{
return $this->isDomainName ??= match (true) {
HostType::RegisteredName !== $this->type, '' === $this->value => false,
null === $this->value => true,
default => is_object($result = IdnConverter::toAscii($this->value))
&& !$result->hasErrors()
&& self::isValidDomain($result->domain()),
};
}
public function ipVersion(): ?string
{
if (!$this->isIpVersionLoaded) {
$this->isIpVersionLoaded = true;
$this->ipVersion = match (true) {
HostType::Ipv4 === $this->type => '4',
HostType::Ipv6 === $this->type => '6',
1 === preg_match(self::REGEXP_IP_FUTURE, substr((string) $this->value, 1, -1), $matches) => $matches['version'],
default => null,
};
}
return $this->ipVersion;
}
public function ipValue(): ?string
{
if (!$this->isIpValueLoaded) {
$this->isIpValueLoaded = true;
$this->ipValue = (function (): ?string {
if (HostType::RegisteredName === $this->type) {
return null;
}
if (HostType::Ipv4 === $this->type) {
return $this->value;
}
$ip = substr((string) $this->value, 1, -1);
if (HostType::Ipv6 !== $this->type) {
return substr($ip, (int) strpos($ip, '.') + 1);
}
$pos = strpos($ip, '%');
if (false === $pos) {
return $ip;
}
return substr($ip, 0, $pos).'%'.rawurldecode(substr($ip, $pos + 3));
})();
}
return $this->ipValue;
}
public static function isValid(BackedEnum|Stringable|string|null $host): bool
{
try {
HostRecord::from($host);
return true;
} catch (Throwable) {
return false;
}
}
public static function isIpv4(Stringable|string|null $host): bool
{
try {
return HostType::Ipv4 === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIpv6(Stringable|string|null $host): bool
{
try {
return HostType::Ipv6 === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIpvFuture(Stringable|string|null $host): bool
{
try {
return HostType::IpvFuture === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIp(Stringable|string|null $host): bool
{
return !self::isRegisteredName($host);
}
public static function isRegisteredName(Stringable|string|null $host): bool
{
try {
return HostType::RegisteredName === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isDomain(Stringable|string|null $host): bool
{
try {
return HostRecord::from($host)->isDomainType();
} catch (Throwable) {
return false;
}
}
/**
* @throws SyntaxError
*/
public static function from(BackedEnum|Stringable|string|null $host): self
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
if ($host instanceof UriComponentInterface) {
$host = $host->value();
}
if (null === $host) {
return new self(
value: null,
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
$host = (string) $host;
if ('' === $host) {
return new self(
value: '',
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
static $inMemoryCache = [];
if (isset($inMemoryCache[$host])) {
return $inMemoryCache[$host];
}
if (self::MAXIMUM_HOST_CACHED < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
if ($host === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv4,
format: HostFormat::Ascii,
);
}
if (str_starts_with($host, '[')) {
str_ends_with($host, ']') || throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
$ipHost = substr($host, 1, -1);
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
return !in_array($matches['version'], ['4', '6'], true) ? ($inMemoryCache[$host] = new self(
value: $host,
type: HostType::IpvFuture,
format: HostFormat::Ascii,
)) : throw new SyntaxError('The host '.$host.' is not a valid IPvFuture host.');
}
if (self::isValidIpv6Hostname($ipHost)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv6,
format: HostFormat::Ascii,
);
}
throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
}
$domainName = rawurldecode($host);
$format = HostFormat::Unicode;
if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
$domainName = strtolower($domainName);
$format = HostFormat::Ascii;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
(HostFormat::Ascii !== $format && 1 !== preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) || throw new SyntaxError('`'.$host.'` is an invalid domain name : the host contains invalid characters.');
IdnConverter::toAsciiOrFail($domainName);
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
/**
* Tells whether the registered name is a valid domain name according to RFC1123.
*
* @see http://man7.org/linux/man-pages/man7/hostname.7.html
* @see https://tools.ietf.org/html/rfc1123#section-2.1
*/
private static function isValidDomain(string $hostname): bool
{
$domainMaxLength = str_ends_with($hostname, '.') ? 254 : 253;
return !isset($hostname[$domainMaxLength])
&& 1 === preg_match(self::REGEXP_DOMAIN_NAME, $hostname);
}
/**
* Validates an Ipv6 as Host.
*
* @see http://tools.ietf.org/html/rfc6874#section-2
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
private static function isValidIpv6Hostname(string $host): bool
{
[$ipv6, $scope] = explode('%', $host, 2) + [1 => null];
if (null === $scope) {
return (bool) filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
$scope = rawurldecode('%'.$scope);
return 1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $scope)
&& 1 !== preg_match(self::REGEXP_GEN_DELIMS, $scope)
&& false !== filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton((string)$ipv6), self::ADDRESS_BLOCK);
}
public function jsonSerialize(): ?string
{
return $this->value;
}
/**
* @return HostRecordSerializedShape
*/
public function __serialize(): array
{
return [['host' => $this->value], []];
}
/**
* @param HostRecordSerializedShape $data
*
* @throws Exception|SyntaxError
*/
public function __unserialize(array $data): void
{
[$properties] = $data;
$record = self::from($properties['host'] ?? throw new Exception('The `host` property is missing from the serialized object.'));
//if the Host computed value are already cache this avoid recomputing them
foreach (get_object_vars($record) as $prop => $value) {
/* @phpstan-ignore-next-line */
$this->{$prop} = $value;
}
}
}

22
vendor/league/uri-interfaces/HostType.php vendored Executable file
View File

@@ -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;
enum HostType
{
case RegisteredName;
case Ipv4;
case Ipv6;
case IpvFuture;
}

View File

@@ -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, mixed $value2): int
{
return bccomp((string) $value1, (string) $value2, self::SCALE);
}
public function multiply(mixed $value1, mixed $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);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,318 @@
<?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 BackedEnum;
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 hexdec;
use function long2ip;
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(BackedEnum|Stringable|string|null $host): bool
{
if ($host instanceof BackedEnum) {
$host = (string) $host->value;
}
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));
if (count($hexParts) < 2) {
return false;
}
$ipAddress = long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1]));
return '' !== ''.$ipAddress;
}
public function toIPv6Using6to4(BackedEnum|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(BackedEnum|Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
return '['.self::IPV4_MAPPED_PREFIX.$host.']';
}
public function toOctal(BackedEnum|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(BackedEnum|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(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
$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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,178 @@
<?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 BackedEnum;
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(BackedEnum|string $ipAddress): string
{
if ($ipAddress instanceof BackedEnum) {
$ipAddress = (string) $ipAddress->value;
}
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(BackedEnum|string $ipAddress): string
{
if ($ipAddress instanceof BackedEnum) {
$ipAddress = (string) $ipAddress->value;
}
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(BackedEnum|Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match (true) {
null === $host => $host,
$host instanceof BackedEnum => (string) $host->value,
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);
}
public 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'],
}.']';
}
/**
* @return array{ipAddress:string|null, zoneIdentifier:string|null}
*/
private static function parse(BackedEnum|Stringable|string|null $host): array
{
if (null === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if ($host instanceof BackedEnum) {
$host = $host->value;
}
$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],
};
}
/**
* Tells whether the host is an IPv6.
*/
public static function isIpv6(BackedEnum|Stringable|string|null $host): bool
{
return null !== self::parse($host)['ipAddress'];
}
public static function normalize(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
if (null === $host || '' === $host) {
return $host;
}
$host = (string) $host;
$components = self::parse($host);
if (null === $components['ipAddress']) {
return strtolower($host);
}
$components['ipAddress'] = strtolower($components['ipAddress']);
return self::build($components);
}
}

View File

@@ -0,0 +1,231 @@
<?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 BackedEnum;
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 function strtolower;
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))*)
)
^(?:(?&reg_name)\.)*(?&reg_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(BackedEnum|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(BackedEnum|Stringable|string $domain, Option|int|null $options = null): Result
{
if ($domain instanceof BackedEnum) {
$domain = $domain->value;
}
$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(BackedEnum|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(BackedEnum|Stringable|string $domain, Option|int|null $options = null): Result
{
if ($domain instanceof BackedEnum) {
$domain = $domain->value;
}
$domain = rawurldecode((string) $domain);
if (false === stripos($domain, 'xn--')) {
return Result::fromIntl(['result' => strtolower($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' => strtolower($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 yields the same result
*/
public static function isIdn(BackedEnum|Stringable|string|null $domain): bool
{
if ($domain instanceof BackedEnum) {
$domain = $domain->value;
}
$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;
}
}

64
vendor/league/uri-interfaces/Idna/Error.php vendored Executable file
View File

@@ -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)
)
);
}
}

179
vendor/league/uri-interfaces/Idna/Option.php vendored Executable file
View File

@@ -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),
};
}
}

64
vendor/league/uri-interfaces/Idna/Result.php vendored Executable file
View File

@@ -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);
}
}

View File

@@ -0,0 +1,188 @@
<?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 BackedEnum;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\StringCoercionMode;
use Stringable;
use function array_combine;
use function explode;
use function implode;
use function is_string;
use function preg_match;
use function str_replace;
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(BackedEnum|Stringable|string|int|float|bool|null $value): array
{
$value = StringCoercionMode::Native->coerce($value);
if (null === $value) {
return [];
}
$value = match (1) {
preg_match(self::REGEXP_INVALID_CHARS, $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'),
default => str_replace($this->toEncoding, $this->fromRfc3986, $value),
};
return array_map(
fn (string $pair): array => explode('=', $pair, 2) + [1 => null],
explode($this->separator, $value)
);
}
/**
* @param iterable<array{0:string|null, 1:BackedEnum|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] => StringCoercionMode::Native->coerce($pair[0]),
default => StringCoercionMode::Native->coerce($pair[0]).'='.StringCoercionMode::Native->coerce($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,
};
}
}

20
vendor/league/uri-interfaces/LICENSE vendored Executable file
View File

@@ -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.

View File

@@ -0,0 +1,88 @@
<?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;
enum QueryComposeMode
{
/**
* Pre-PHP 8.4 Mode.
*
* Strictly uses get_object_vars on objects (Enum included)
* If the value can not be serialized the entry is skipped.
*
* ie http_build_query behavior before PHP8.4
*/
case Compatible;
/**
* PHP 8.4+ enum-compatible lenient mode.
*
* Provides stable support for BackedEnum values.
* UnitEnum values are skipped.
* Uses get_object_vars() for non-enum objects.
* Unserializable values are skipped.
*
* Behaves like {@see QueryComposeMode::EnumCompatible}
* but does not throw for UnitEnum values.
*
* Mirrors http_build_query behavior in PHP 8.4+,
* except that error cases are silently ignored
* instead of throwing.
*
* This mode is tolerant by design and skips entries that would otherwise
* result in an exception in {@see QueryComposeMode::EnumCompatible}.
*/
case EnumLenient;
/**
* PHP 8.4+ mode.
*
* Provides stable support for BackedEnum values.
* Throws for UnitEnum.
* Uses get_object_vars() for non-enum objects.
* Unserializable values are skipped.
*
* http_build_query behavior in PHP 8.4+.
*/
case EnumCompatible;
/**
* Use PHP version http_build_query algorithm.
*
* In pre-PHP8.4 you get the same results as `Compatible`
* In PHP PHP8.4+ you get the same results as `EnumCompatible`
*/
case Native;
/**
* Validation-first mode.
*
* Guarantees that only scalar values, BackedEnum, and null are accepted.
* Any object, UnitEnum, resource, or recursive structure
* results in an exception.
*
* - null: the key name is used but the separator and its content are omitted
* - string: used as-is
* - bool: converted to string “0” (false) or “1” (true)
* - int: converted to numeric string (123 -> “123”)
* - float: converted to decimal string (3.14 -> “3.14”)
* - Backed Enum: converted to their backing value and then stringify see int and string
* - array: empty array: An empty array has zero items, therefore empty arrays are omitted from the query parameter list.
* - lists: Becomes a repeated name suffixed with empty brackets (ie "a" with ["foo", false, 1.23] will result in a[]=foo&a[]=0&a[]=1.23)
* - maps: Becomes a repeated name suffixed with brackets containing the key (ie "a" with ["b" => "foo", "c" => false, "d" => 1.23] will result in a[b]=foo&a[c]=0&a[d]=1.23)
*
* This contract is stable and independent of PHP's http_build_query implementation.
*/
case Safe;
}

View File

@@ -0,0 +1,50 @@
<?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;
enum QueryExtractMode
{
/**
* Parses the query string using parse_str algorithm.
*/
case Native;
/**
* Parses the query string like parse_str without mangling result keys.
*
* The result is similar to PHP parse_str when used with its second argument,
* with the difference that variable names are not mangled.
*
* Behavior details:
* - Empty names are ignored
* - If a name is duplicated, the last value overwrites the previous one
* - If no "[" is detected, the value is added using the name as the array key
* - If "[" is detected but no matching "]" exists, the value is added using the name as the array key
* - If bracket usage is malformed, the remaining part is dropped
* - "." and " " are NOT converted to "_"
* - If no "]" exists, the first "[" is not converted to "_"
* - No whitespace trimming is performed on keys
*
* @see https://www.php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*/
case Unmangled;
/**
* Same as QueryParsingMode::Unmangled and additionally
* preserves null values instead of converting them
* to empty strings.
*/
case LossLess;
}

495
vendor/league/uri-interfaces/QueryString.php vendored Executable file
View File

@@ -0,0 +1,495 @@
<?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 BackedEnum;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use ReflectionEnum;
use ReflectionException;
use SplObjectStorage;
use Stringable;
use TypeError;
use UnitEnum;
use ValueError;
use function array_is_list;
use function array_key_exists;
use function array_keys;
use function get_debug_type;
use function get_object_vars;
use function http_build_query;
use function implode;
use function is_array;
use function is_object;
use function is_resource;
use function is_scalar;
use function rawurldecode;
use function str_replace;
use function strpos;
use function substr;
use const PHP_QUERY_RFC1738;
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;
private const RECURSION_MARKER = "\0__RECURSION_INTERNAL_MARKER__\0";
/**
* @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:mixed}> $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, StringCoercionMode $coercionMode = StringCoercionMode::Native): ?string
{
return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator), $coercionMode);
}
/**
* 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:mixed}> $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, StringCoercionMode $coercionMode = StringCoercionMode::Native): ?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.');
}
[$key, $value] = $pair;
$coercionMode->isCoercible($value) || throw new SyntaxError('Converting a type `'.get_debug_type($value).'` into a string is not supported by the '.(StringCoercionMode::Native === $coercionMode ? 'PHP Native' : 'Ecmascript').' coercion mode.');
try {
$key = $coercionMode->coerce($key);
$value = $coercionMode->coerce($value);
} catch (TypeError $typeError) {
throw new SyntaxError('The pair can not be converted to build a query string.', previous: $typeError);
}
$keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($key), null === $value ? null : Encoder::encodeQueryKeyValue($value)];
}
return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs);
}
/**
* Build a query string from an object or an array like http_build_query without discarding values.
* The method differs from http_build_query for the following behavior:
*
* - if a resource is used, a TypeError is thrown.
* - if a recursion is detected a ValueError is thrown
* - the method preserves value with `null` value (http_build_query) skip the key.
* - the method does not handle prefix usage
*
* @param array<array-key, mixed> $data
* @param non-empty-string $separator
*
* @throws TypeError if a resource is found it the input array
* @throws ValueError if a recursion is detected
*/
public static function compose(
array|object $data,
string $separator = '&',
int $encType = PHP_QUERY_RFC1738,
QueryComposeMode $composeMode = QueryComposeMode::Native
): ?string {
if (QueryComposeMode::Native === $composeMode) {
return http_build_query(data: $data, arg_separator: $separator, encoding_type: $encType);
}
$query = self::composeFromValue($data, Converter::fromEncodingType($encType)->withSeparator($separator), $composeMode);
return QueryComposeMode::Safe !== $composeMode ? (string) $query : $query;
}
public static function composeFromValue(
array|object $data,
?Converter $converter = null,
QueryComposeMode $composeMode = QueryComposeMode::Native,
): ?string {
if (QueryComposeMode::EnumLenient === $composeMode && $data instanceof UnitEnum && !$data instanceof BackedEnum) {
return '';
}
QueryComposeMode::Safe !== $composeMode || is_array($data) || throw new TypeError('In safe mode only arrays are supported.');
$converter ??= Converter::fromRFC3986();
$pairs = QueryComposeMode::Native !== $composeMode
? self::composeRecursive($composeMode, $data)
: self::parseFromValue(http_build_query(data: $data, arg_separator: '&'), Converter::fromRFC1738());
return self::buildFromPairs($pairs, $converter);
}
/**
* @param array<array-key, mixed>|object $data
* @param SplObjectStorage<object, null> $seenObjects
*
* @throws TypeError if a resource is found it the input array
* @throws ValueError if a recursion is detected
* @throws ReflectionException if reflection is not possible on the Enum
*
* @return iterable<array{0: array-key, 1: string|int|float|bool|null}>
*/
private static function composeRecursive(
QueryComposeMode $composeMode,
array|object $data,
string|int $prefix = '',
SplObjectStorage $seenObjects = new SplObjectStorage(),
): iterable {
QueryComposeMode::Safe !== $composeMode || is_array($data) || throw new TypeError('In safe mode only arrays are supported.');
in_array($composeMode, [QueryComposeMode::EnumCompatible, QueryComposeMode::EnumLenient], true) || !$data instanceof UnitEnum || throw new TypeError('Argument #1 ($data) must not be an enum, '.((new ReflectionEnum($data::class))->isBacked() ? 'Backed' : 'Pure').' given') ;
if (is_object($data)) {
if ($seenObjects->contains($data)) {
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.');
return;
}
$seenObjects->attach($data);
$data = get_object_vars($data);
}
if (self::hasCircularReference($data)) {
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.');
return;
}
$stripIndices = QueryComposeMode::Safe === $composeMode && array_is_list($data);
foreach ($data as $name => $value) {
$name = $stripIndices ? '' : $name;
if ('' !== $prefix) {
$name = $prefix.'['.$name.']';
}
if (is_resource($value)) {
QueryComposeMode::Safe !== $composeMode || throw new TypeError('composition failed; a resource has been detected and can not be converted.');
continue;
}
if (is_scalar($value)) {
yield [$name, $value];
continue;
}
if (null === $value) {
if (QueryComposeMode::Safe === $composeMode) {
yield [$name, $value];
}
continue;
}
if ($value instanceof BackedEnum) {
if (QueryComposeMode::Compatible !== $composeMode) {
yield [$name, $value->value];
continue;
}
$value = get_object_vars($value);
}
if ($value instanceof UnitEnum) {
if (QueryComposeMode::EnumLenient === $composeMode) {
continue;
}
QueryComposeMode::Compatible === $composeMode || throw new TypeError('Unbacked enum '.$value::class.' cannot be converted to a string');
$value = get_object_vars($value);
}
if (QueryComposeMode::Safe === $composeMode && is_object($value)) {
throw new ValueError('In conservative mode only arrays, scalar value or null are supported.');
}
yield from self::composeRecursive($composeMode, $value, $name, $seenObjects);
}
}
/**
* Array recursion detection.
* @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
*/
private static function hasCircularReference(array &$arr): bool
{
if (isset($arr[self::RECURSION_MARKER])) {
return true;
}
try {
$arr[self::RECURSION_MARKER] = true;
foreach ($arr as $key => &$value) {
if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
return true;
}
}
return false;
} finally {
unset($arr[self::RECURSION_MARKER]);
}
}
/**
* Parses the query string.
*
* The result depends on the query parsing mode
*
* @see QueryString::extractFromValue()
*
* @param non-empty-string $separator
*
* @throws SyntaxError
*/
public static function extract(
BackedEnum|Stringable|string|bool|null $query,
string $separator = '&',
int $encType = PHP_QUERY_RFC3986,
QueryExtractMode $extractMode = QueryExtractMode::Unmangled,
): array {
return self::extractFromValue(
$query,
Converter::fromEncodingType($encType)->withSeparator($separator),
$extractMode,
);
}
/**
* Parses the query string.
*
* The result depends on the query parsing mode
*
* @throws SyntaxError
*/
public static function extractFromValue(
BackedEnum|Stringable|string|bool|null $query,
?Converter $converter = null,
QueryExtractMode $extractMode = QueryExtractMode::Unmangled,
): array {
$pairs = ($converter ?? Converter::fromRFC3986())->toPairs($query);
if (QueryExtractMode::Native === $extractMode) {
if ([] === $pairs) {
return [];
}
$data = [];
foreach ($pairs as [$key, $value]) {
$key = str_replace('&', '%26', (string) $key);
$data[] = null === $value ? $key : $key.'='.str_replace('&', '%26', $value);
}
parse_str(implode('&', $data), $result);
return $result;
}
return self::convert(
self::decodePairs($pairs, self::PAIR_VALUE_PRESERVED),
$extractMode
);
}
/**
* 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(BackedEnum|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(BackedEnum|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, QueryExtractMode $extractMode = QueryExtractMode::Unmangled): array
{
$returnedValue = [];
foreach ($pairs as $pair) {
$returnedValue = self::extractPhpVariable($returnedValue, $pair, extractMode: $extractMode);
}
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 = '',
QueryExtractMode $extractMode = QueryExtractMode::Unmangled
): array {
if (is_array($name)) {
[$name, $value] = $name;
if (null !== $value || QueryExtractMode::LossLess !== $extractMode) {
$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, $extractMode);
return $data;
}
}

View File

@@ -0,0 +1,158 @@
<?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 BackedEnum;
use League\Uri\Contracts\UriComponentInterface;
use Stringable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use ValueError;
use function array_is_list;
use function array_map;
use function get_debug_type;
use function implode;
use function is_array;
use function is_float;
use function is_infinite;
use function is_nan;
use function is_object;
use function is_resource;
use function is_scalar;
use function json_encode;
use const JSON_PRESERVE_ZERO_FRACTION;
enum StringCoercionMode
{
/**
* PHP conversion mode.
*
* Guarantees that only scalar values, BackedEnum, and null are accepted.
* Any object, UnitEnum, resource, or recursive structure
* results in an error.
*
* - null: is not converted and stays the `null` value
* - string: used as-is
* - bool: converted to string “0” (false) or “1” (true)
* - int: converted to numeric string (123 -> “123”)
* - float: converted to decimal string (3.14 -> “3.14”)
* - Backed Enum: converted to their backing value and then stringify see int and string
*/
case Native;
/**
* Ecmascript conversion mode.
*
* Guarantees that only scalar values, BackedEnum, and null are accepted.
* Any resource, or recursive structure results in an error.
*
* - null: converted to string “null”
* - string: used as-is
* - bool: converted to string “false” (false) or “true” (true)
* - int: converted to numeric string (123 -> “123”)
* - float: converted to decimal string (3.14 -> “3.14”), "NaN", "-Infinity" or "Infinity"
* - Backed Enum: converted to their backing value and then stringify see int and string
* - Array as list are flatten into a string list using the "," character as separator
* - Associative array, Unit Enum, any object without stringification semantics is coerced to "[object Object]".
*/
case Ecmascript;
private const RECURSION_MARKER = "\0__RECURSION_INTERNAL_MARKER_WHATWG__\0";
public function isCoercible(mixed $value): bool
{
return self::Ecmascript === $this
? !is_resource($value)
: match (true) {
$value instanceof Rfc3986Uri,
$value instanceof WhatWgUrl,
$value instanceof BackedEnum,
$value instanceof Stringable,
is_scalar($value),
null === $value => true,
default => false,
};
}
/**
* @throws TypeError if the type is not supported by the specific case
* @throws ValueError if circular reference is detected
*/
public function coerce(mixed $value): ?string
{
return match ($this) {
self::Ecmascript => match (true) {
$value instanceof Rfc3986Uri => $value->toString(),
$value instanceof WhatWgUrl => $value->toAsciiString(),
$value instanceof BackedEnum => (string) $value->value,
$value instanceof Stringable => $value->__toString(),
is_object($value) => '[object Object]',
is_array($value) => match (true) {
self::hasCircularReference($value) => throw new ValueError('Recursive array structure detected; unable to coerce value.'),
array_is_list($value) => implode(',', array_map($this->coerce(...), $value)),
default => '[object Object]',
},
true === $value => 'true',
false === $value => 'false',
null === $value => 'null',
is_float($value) => match (true) {
is_nan($value) => 'NaN',
is_infinite($value) => 0 < $value ? 'Infinity' : '-Infinity',
default => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
},
is_scalar($value) => (string) $value,
default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
},
self::Native => match (true) {
$value instanceof UriComponentInterface => $value->value(),
$value instanceof WhatWgUrl => $value->toAsciiString(),
$value instanceof Rfc3986Uri => $value->toString(),
$value instanceof BackedEnum => (string) $value->value,
$value instanceof Stringable => $value->__toString(),
false === $value => '0',
true === $value => '1',
null === $value => null,
is_scalar($value) => (string) $value,
default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
},
};
}
/**
* Array recursion detection.
* @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
*/
private static function hasCircularReference(array &$arr): bool
{
if (isset($arr[self::RECURSION_MARKER])) {
return true;
}
try {
$arr[self::RECURSION_MARKER] = true;
foreach ($arr as $key => &$value) {
if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
return true;
}
}
return false;
} finally {
unset($arr[self::RECURSION_MARKER]);
}
}
}

View File

@@ -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;
enum UriComparisonMode
{
case IncludeFragment;
case ExcludeFragment;
}

755
vendor/league/uri-interfaces/UriString.php vendored Executable file
View File

@@ -0,0 +1,755 @@
<?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 BackedEnum;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnaConverter;
use Stringable;
use Throwable;
use function array_map;
use function array_merge;
use function array_pop;
use function array_reduce;
use function defined;
use function explode;
use function filter_var;
use function function_exists;
use function implode;
use function preg_match;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
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 = [
'' => ['path' => ''],
'#' => ['fragment' => ''],
'?' => ['query' => ''],
'?#' => ['query' => '', 'fragment' => ''],
'/' => ['path' => '/'],
'//' => ['host' => ''],
'///' => ['host' => '', 'path' => '/'],
];
/**
* Range of invalid characters in URI 3986 string.
*
* @var string
*/
private const REGEXP_VALID_URI_RFC3986_CHARS = '/^(?:[A-Za-z0-9\-._~:\/?#[\]@!$&\'()*+,;=%]|%[0-9A-Fa-f]{2})*$/';
/**
* Range of invalid characters in URI 3987 string.
*
* @var string
*/
private const REGEXP_INVALID_URI_RFC3987_CHARS = '/[\x00-\x1f\x7f\s]/';
/**
* 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';
/**
* 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>.*))?$,';
/** @var array<string,int> */
private const DOT_SEGMENTS = ['.' => 1, '..' => 1];
/**
* Generate an IRI string representation (RFC3987) 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
*/
public static function toIriString(BackedEnum|Stringable|string $uri): string
{
$components = UriString::parse($uri);
$port = null;
if (isset($components['port'])) {
$port = (int) $components['port'];
unset($components['port']);
}
if (null !== $components['host']) {
$components['host'] = IdnaConverter::toUnicode($components['host'])->domain();
}
$components['path'] = Encoder::decodePath($components['path']);
$components['user'] = Encoder::decodeNecessary($components['user']);
$components['pass'] = Encoder::decodeNecessary($components['pass']);
$components['query'] = Encoder::decodeQuery($components['query']);
$components['fragment'] = Encoder::decodeFragment($components['fragment']);
return self::build([
...array_map(fn (?string $value) => match (true) {
null === $value,
!str_contains($value, '%20') => $value,
default => str_replace('%20', ' ', $value),
}, $components),
...['port' => $port],
]);
}
/**
* 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'] ?? null,
$components['query'] ?? null,
$components['fragment'] ?? null,
);
}
/**
* Generates 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 = null,
?string $authority = null,
?string $path = null,
?string $query = null,
?string $fragment = null,
): string {
self::validateComponents($scheme, $authority, $path);
$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'])) {
(!isset($components['user']) && !isset($components['pass'])) || throw new SyntaxError('The user info component must not be set if the host is not defined.');
!isset($components['port']) || throw new SyntaxError('The port component must not be set if the host is not defined.');
return null;
}
$userInfo = $components['user'] ?? null;
if (isset($components['pass'])) {
$userInfo .= ':'.$components['pass'];
}
$authority = '';
if (isset($userInfo)) {
$authority .= $userInfo.'@';
}
$authority .= $components['host'];
if (isset($components['port'])) {
$authority .= ':'.$components['port'];
}
return $authority;
}
/**
* Parses and normalizes the URI following RFC3986 destructive and non-destructive constraints.
*
* @throws SyntaxError if the URI is not parsable
*
* @return ComponentMap
*/
public static function parseNormalized(Stringable|string $uri): array
{
$components = self::parse($uri);
if (null !== $components['scheme']) {
$components['scheme'] = strtolower($components['scheme']);
}
$components['host'] = self::normalizeHost($components['host']);
$path = $components['path'];
$authority = self::buildAuthority($components);
//dot segment only happens when:
// - the path is absolute
// - the scheme and/or the authority are defined
if ('/' === ($path[0] ?? '') || '' !== $components['scheme'].$authority) {
$path = self::removeDotSegments($path);
}
// if there is an authority, the path must be absolute
if ('' !== $path && '/' !== $path[0]) {
if (null !== $authority) {
$path = '/'.$path;
}
}
$components['path'] = (string) Encoder::normalizePath($path);
$components['query'] = Encoder::normalizeQuery($components['query']);
$components['fragment'] = Encoder::normalizeFragment($components['fragment']);
$components['user'] = Encoder::normalizeUser($components['user']);
$components['pass'] = Encoder::normalizePassword($components['pass']);
return $components;
}
/**
* Parses and normalizes the URI following RFC3986 destructive and non-destructive constraints.
*
* @throws SyntaxError if the URI is not parsable
*/
public static function normalize(Stringable|string $uri): string
{
return self::build(self::parseNormalized($uri));
}
/**
* Parses and normalizes the URI following RFC3986 destructive and non-destructive constraints.
*
* @throws SyntaxError if the URI is not parsable
*/
public static function normalizeAuthority(Stringable|string|null $authority): ?string
{
if (null === $authority) {
return null;
}
$components = UriString::parseAuthority($authority);
$components['host'] = self::normalizeHost($components['host'] ?? null);
$components['user'] = Encoder::normalizeUser($components['user']);
$components['pass'] = Encoder::normalizePassword($components['pass']);
return (string) self::buildAuthority($components);
}
/**
* 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.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-5
*
* @throws SyntaxError if the BaseUri is not absolute or in absence of a BaseUri if the uri is not absolute
*/
public static function resolve(BackedEnum|Stringable|string $uri, BackedEnum|Stringable|string|null $baseUri = null): string
{
if ($uri instanceof BackedEnum) {
$uri = (string) $uri->value;
}
if ($baseUri instanceof BackedEnum) {
$baseUri = (string) $baseUri->value;
}
$uri = (string) $uri;
if ('' === $uri) {
$uri = $baseUri ?? throw new SyntaxError('The uri can not be the empty string when there\'s no base URI.');
}
$uriComponents = self::parse($uri);
$baseUriComponents = $uriComponents;
if (null !== $baseUri && (string) $uri !== (string) $baseUri) {
$baseUriComponents = self::parse($baseUri);
}
$hasLeadingSlash = str_starts_with($baseUriComponents['path'], '/');
if (null === $baseUriComponents['scheme']) {
throw new SyntaxError('The base URI must be an absolute URI or null; If the base URI is null the URI must be an absolute URI.');
}
if (null !== $uriComponents['scheme'] && '' !== $uriComponents['scheme']) {
$uriComponents['path'] = self::removeDotSegments($uriComponents['path']);
if ('' !== $uriComponents['path'] && '/' !== $uriComponents['path'][0] && $hasLeadingSlash) {
$uriComponents['path'] = '/'.$uriComponents['path'];
}
return UriString::build($uriComponents);
}
if (null !== self::buildAuthority($uriComponents)) {
$uriComponents['scheme'] = $baseUriComponents['scheme'];
$uriComponents['path'] = self::removeDotSegments($uriComponents['path']);
if ('' !== $uriComponents['path'] && '/' !== $uriComponents['path'][0] && $hasLeadingSlash) {
$uriComponents['path'] = '/'.$uriComponents['path'];
}
return UriString::build($uriComponents);
}
[$path, $query] = self::resolvePathAndQuery($uriComponents, $baseUriComponents);
$path = UriString::removeDotSegments($path);
if ('' !== $path && '/' !== $path[0] && $hasLeadingSlash) {
$path = '/'.$path;
}
$baseUriComponents['path'] = $path;
$baseUriComponents['query'] = $query;
$baseUriComponents['fragment'] = $uriComponents['fragment'];
return UriString::build($baseUriComponents);
}
/**
* Filter Dot segment according to RFC3986.
*
* @see http://tools.ietf.org/html/rfc3986#section-5.2.4
*/
public static function removeDotSegments(Stringable|string $path): string
{
$path = (string) $path;
if (!str_contains($path, '.')) {
return $path;
}
$reducer = function (array $carry, string $segment): array {
if ('..' === $segment) {
array_pop($carry);
return $carry;
}
if (!isset(self::DOT_SEGMENTS[$segment])) {
$carry[] = $segment;
}
return $carry;
};
$oldSegments = explode('/', $path);
$newPath = implode('/', array_reduce($oldSegments, $reducer(...), []));
if (isset(self::DOT_SEGMENTS[$oldSegments[array_key_last($oldSegments)]])) {
$newPath .= '/';
}
return $newPath;
}
/**
* Resolves an URI path and query component.
*
* @param ComponentMap $uri
* @param ComponentMap $baseUri
*
* @return array{0:string, 1:string|null}
*/
private static function resolvePathAndQuery(array $uri, array $baseUri): array
{
if (str_starts_with($uri['path'], '/')) {
return [$uri['path'], $uri['query']];
}
if ('' === $uri['path']) {
return [$baseUri['path'], $uri['query'] ?? $baseUri['query']];
}
$targetPath = $uri['path'];
if (null !== self::buildAuthority($baseUri) && '' === $baseUri['path']) {
$targetPath = '/'.$targetPath;
}
if ('' !== $baseUri['path']) {
$segments = explode('/', $baseUri['path']);
array_pop($segments);
if ([] !== $segments) {
$targetPath = implode('/', $segments).'/'.$targetPath;
}
}
return [$targetPath, $uri['query']];
}
public static function containsRfc3986Chars(Stringable|string $uri): bool
{
return 1 === preg_match(self::REGEXP_VALID_URI_RFC3986_CHARS, (string) $uri);
}
public static function containsRfc3987Chars(Stringable|string $uri): bool
{
return 1 !== preg_match(self::REGEXP_INVALID_URI_RFC3987_CHARS, (string) $uri);
}
/**
* 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(BackedEnum|Stringable|string|int $uri): array
{
if ($uri instanceof BackedEnum) {
$uri = $uri->value;
}
$uri = (string) $uri;
if (isset(self::URI_SHORTCUTS[$uri])) {
/** @var ComponentMap $components */
$components = [...self::URI_COMPONENTS, ...self::URI_SHORTCUTS[$uri]];
return $components;
}
self::containsRfc3987Chars($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;
}
/**
* 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
*/
private static function validateComponents(?string $scheme, ?string $authority, ?string $path): void
{
if (null !== $authority) {
if (null !== $path && '' !== $path && '/' !== $path[0]) {
throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.');
}
return;
}
if (null === $path || '' === $path) {
return;
}
if (str_starts_with($path, '//')) {
throw new SyntaxError('If there is no authority the path `'.$path.'` cannot start with a `//`.');
}
if (null !== $scheme || false === ($pos = strpos($path, ':'))) {
return;
}
if (!str_contains(substr($path, 0, $pos), '/')) {
throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
}
}
/**
* 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(BackedEnum|Stringable|string|null $authority): array
{
$components = ['user' => null, 'pass' => null, 'host' => null, 'port' => null];
if (null === $authority) {
return $components;
}
if ($authority instanceof BackedEnum) {
$authority = $authority->value;
}
$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(Stringable|string|null $host): ?string
{
try {
return HostRecord::from($host)->value;
} catch (Throwable) {
throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
}
}
/**
* Tells whether the scheme component is valid.
*/
public static function isValidScheme(BackedEnum|Stringable|string|null $scheme): bool
{
if ($scheme instanceof BackedEnum) {
$scheme = $scheme->value;
}
return null === $scheme || 1 === preg_match('/^[A-Za-z]([-A-Za-z\d+.]+)?$/', (string) $scheme);
}
private static function normalizeHost(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
if (null !== $host) {
$host = (string) $host;
}
if (null === $host || false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $host;
}
$host = (string) Encoder::normalizeHost($host);
static $isSupported = null;
$isSupported ??= (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46'));
if (! $isSupported) {
return $host;
}
$idnaHost = IdnaConverter::toAscii($host);
if (!$idnaHost->hasErrors()) {
return $idnaHost->domain();
}
return $host;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see HostRecoord::validate()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\HostRecord::validate() instead', since:'league/uri:7.6.0')]
public static function isValidHost(Stringable|string|null $host): bool
{
return HostRecord::isValid($host);
}
}

View File

@@ -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;
enum UrnComparisonMode
{
case IncludeComponents;
case ExcludeComponents;
}

70
vendor/league/uri-interfaces/composer.json vendored Executable file
View File

@@ -0,0 +1,70 @@
{
"name": "league/uri-interfaces",
"type": "library",
"description" : "Common tools for parsing and resolving RFC3987/RFC3986 URI",
"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"
},
"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",
"rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification"
},
"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
}
}