Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 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,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,117 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Countable;
use Iterator;
use IteratorAggregate;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
/**
* @extends IteratorAggregate<string>
*/
interface DomainHostInterface extends Countable, HostInterface, IteratorAggregate
{
/**
* Returns the labels total number.
*/
public function count(): int;
/**
* Iterate over the Domain labels.
*
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Retrieves a single host label.
*
* If the label offset has not been set, returns the null value.
*/
public function get(int $offset): ?string;
/**
* Returns the associated key for a specific label or all the keys.
*
* @return int[]
*/
public function keys(?string $label = null): array;
/**
* Tells whether the domain is absolute.
*/
public function isAbsolute(): bool;
/**
* Prepends a label to the host.
*/
public function prepend(Stringable|string $label): self;
/**
* Appends a label to the host.
*/
public function append(Stringable|string $label): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Domain.
*/
public function slice(int $offset, ?int $length = null): self;
/**
* Returns an instance with its Root label.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function withRootLabel(): self;
/**
* Returns an instance without its Root label.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function withoutRootLabel(): self;
/**
* Returns an instance with the modified label.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new label
*
* If $key is non-negative, the added label will be the label at $key position from the start.
* If $key is negative, the added label will be the label at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withLabel(int $key, Stringable|string $label): self;
/**
* Returns an instance without the specified label.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*
* If $key is non-negative, the removed label will be the label at $key position from the start.
* If $key is negative, the removed label will be the label at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withoutLabel(int ...$keys): self;
}

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 FragmentInterface extends UriComponentInterface
{
/**
* Returns the decoded fragment.
*/
public function decoded(): ?string;
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface HostInterface extends UriComponentInterface
{
/**
* Returns the ascii representation.
*/
public function toAscii(): ?string;
/**
* Returns the unicode representation.
*/
public function toUnicode(): ?string;
/**
* Returns the IP version.
*
* If the host is a not an IP this method will return null
*/
public function getIpVersion(): ?string;
/**
* Returns the IP component If the Host is an IP address.
*
* If the host is a not an IP this method will return null
*/
public function getIp(): ?string;
/**
* Tells whether the host is a domain name.
*/
public function isDomain(): bool;
/**
* Tells whether the host is an IP Address.
*/
public function isIp(): bool;
/**
* Tells whether the host is a registered name.
*/
public function isRegisteredName(): bool;
}

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,90 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use League\Uri\Exceptions\SyntaxError;
interface PathInterface extends UriComponentInterface
{
/**
* Returns the decoded path.
*/
public function decoded(): string;
/**
* Tells whether the path is absolute or relative.
*/
public function isAbsolute(): bool;
/**
* Tells whether the path has a trailing slash.
*/
public function hasTrailingSlash(): bool;
/**
* Returns an instance without dot segments.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component normalized by removing
* the dot segment.
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutDotSegments(): self;
/**
* Returns an instance with a leading slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a leading slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withLeadingSlash(): self;
/**
* Returns an instance without a leading slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component without a leading slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutLeadingSlash(): self;
/**
* Returns an instance with a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a trailing slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withTrailingSlash(): self;
/**
* Returns an instance without a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component without a trailing slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutTrailingSlash(): self;
}

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,253 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
use Stringable;
/**
* @extends IteratorAggregate<array{0:string, 1:string|null}>
*
* @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys.
* @method self withoutPairByValue(Stringable|string|int|bool|null ...$values) Returns an instance without pairs with the specified values.
* @method self withoutPairByKeyValue(string $key, Stringable|string|int|bool|null $value) Returns an instance without pairs with the specified key/value pair
* @method bool hasPair(string $key, ?string $value) Tells whether the pair exists in the query.
* @method ?string toFormData() Returns the string representation using the applicat/www-form-urlencoded rules
* @method ?string toRFC3986() Returns the string representation using RFC3986 rules
*/
interface QueryInterface extends Countable, IteratorAggregate, UriComponentInterface
{
/**
* Returns the query separator.
*
* @return non-empty-string
*/
public function getSeparator(): string;
/**
* Returns the number of key/value pairs present in the object.
*/
public function count(): int;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* The pair is represented as an array where the first value is the pair key
* and the second value the pair value.
*
* The key of each pair is a string
* The value of each pair is a scalar or the null value
*
* @return Iterator<int, array{0:string, 1:string|null}>
*/
public function getIterator(): Iterator;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* The return type is as an Iterator where its offset is the pair key and its value the pair value.
*
* The key of each pair is a string
* The value of each pair is a scalar or the null value
*
* @return iterable<string, string|null>
*/
public function pairs(): iterable;
/**
* Tells whether a list of pair with a specific key exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function has(string ...$keys): bool;
/**
* Returns the first value associated to the given pair name.
*
* If no value is found null is returned
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-get
*/
public function get(string $key): ?string;
/**
* Returns all the values associated to the given pair name as an array or all
* the instance pairs.
*
* If no value is found an empty array is returned
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-getall
*
* @return array<int, string|null>
*/
public function getAll(string $key): array;
/**
* Returns the store PHP variables as elements of an array.
*
* The result is similar as PHP parse_str when used with its
* second argument with the difference that variable names are
* not mangled.
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @return array the collection of stored PHP variables or the empty array if no input is given,
*/
public function parameters(): array;
/**
* Returns the value attached to the specific key.
*
* The result is similar to PHP parse_str with the difference that variable
* names are not mangled.
*
* If a key is submitted it will return the value attached to it or null
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @return mixed the collection of stored PHP variables or the empty array if no input is given,
* the single value of a stored PHP variable or null if the variable is not present in the collection
*/
public function parameter(string $name): mixed;
/**
* Tells whether a list of variable with specific names exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function hasParameter(string ...$names): bool;
/**
* Returns the RFC1738 encoded query.
*/
public function toRFC1738(): ?string;
/**
* Returns an instance with a different separator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component with a different separator
*/
public function withSeparator(string $separator): self;
/**
* Returns an instance with the new pairs set to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* @see ::withPair
*/
public function merge(Stringable|string $query): self;
/**
* Returns an instance with the new pairs appended to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* If the pair already exists the value will be added to it.
*/
public function append(Stringable|string $query): self;
/**
* Returns a new instance with a specified key/value pair appended as a new pair.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*/
public function appendTo(string $key, Stringable|string|int|bool|null $value): self;
/**
* Sorts the query string by offset, maintaining offset to data correlations.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-sort
*/
public function sort(): self;
/**
* Returns an instance without duplicate key/value pair.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized by removing
* duplicate pairs whose key/value are the same.
*/
public function withoutDuplicates(): self;
/**
* Returns an instance without empty key/value where the value is the null value.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized by removing
* empty pairs.
*
* A pair is considered empty if its value is equal to the null value
*/
public function withoutEmptyPairs(): self;
/**
* Returns an instance where numeric indices associated to PHP's array like key are removed.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized so that numeric indexes
* are removed from the pair key value.
*
* i.e.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar
*/
public function withoutNumericIndices(): self;
/**
* Returns an instance with a new key/value pair added to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* If the pair already exists the value will replace the existing value.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-set
*/
public function withPair(string $key, Stringable|string|int|float|bool|null $value): self;
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.3.0
* @codeCoverageIgnore
* @see QueryInterface::withoutPairByKey()
*
* Returns an instance without the specified keys.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*/
#[Deprecated(message:'use League\Uri\Contracts\QueryInterface::withoutPairByKey() instead', since:'league/uri-interfaces:7.3.0')]
public function withoutPair(string ...$keys): self;
/**
* Returns an instance without the specified params.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component without PHP's value.
* PHP's mangled is not taken into account.
*/
public function withoutParameters(string ...$names): self;
}

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 $segment): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Path.
*/
public function slice(int $offset, ?int $length = null): self;
/**
* Prepends a segment to the path.
*/
public function prepend(Stringable|string $segment): self;
/**
* Returns an instance with the modified segment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new segment
*
* If $key is non-negative, the added segment will be the segment at $key position from the start.
* If $key is negative, the added segment will be the segment at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withSegment(int $key, Stringable|string $segment): self;
/**
* Returns an instance without the specified segment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*
* If $key is non-negative, the removed segment will be the segment at $key position from the start.
* If $key is negative, the removed segment will be the segment at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withoutSegment(int ...$keys): self;
/**
* Returns an instance without duplicate delimiters.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component normalized by removing
* multiple consecutive empty segment
*/
public function withoutEmptySegments(): self;
/**
* Returns an instance with the specified parent directory's path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withDirname(Stringable|string $path): self;
/**
* Returns an instance with the specified basename.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withBasename(Stringable|string $basename): self;
/**
* Returns an instance with the specified basename extension.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withExtension(Stringable|string $extension): self;
}

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;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
interface UriAccess
{
public function getUri(): UriInterface|Psr7UriInterface;
/**
* Returns the RFC3986 string representation of the complete URI.
*/
public function getUriString(): string;
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use JsonSerializable;
use Stringable;
interface UriComponentInterface extends JsonSerializable, Stringable
{
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined null is returned
*/
public function value(): ?string;
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined an empty string is returned
*/
public function toString(): string;
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined an empty string is returned
*/
public function __toString(): string;
/**
* Returns the instance json representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986 or RFC 1738.
*
* If the instance is not defined null is returned
*/
public function jsonSerialize(): ?string;
/**
* Returns the instance string representation with its optional URI delimiters.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode any
* characters. To determine what characters to encode, please refer to RFC 3986,
* Sections 2 and 3.
*
* If the instance is not defined an empty string is returned
*/
public function getUriComponent(): string;
}

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,314 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use JsonSerializable;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriString;
use Stringable;
/**
* @phpstan-import-type ComponentMap from UriString
*
* @method string|null getUsername() returns the user component of the URI.
* @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource.
* @method array toComponents() returns an associative array containing all the URI components.
*/
interface UriInterface extends JsonSerializable, Stringable
{
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
*/
public function __toString(): string;
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
*/
public function toString(): string;
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @see ::__toString
*/
public function jsonSerialize(): string;
/**
* Retrieve the scheme component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.1.
*
* The trailing ":" character is not part of the scheme and MUST NOT be
* added.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.1
*/
public function getScheme(): ?string;
/**
* Retrieve the authority component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* If the port component is not set or is the standard port for the current
* scheme, it SHOULD NOT be included.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2
*/
public function getAuthority(): ?string;
/**
* Retrieve the user information component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* If a user is present in the URI, this will return that value;
* additionally, if the password is also present, it will be appended to the
* user value, with a colon (":") separating the values.
*
* The trailing "@" character is not part of the user information and MUST
* NOT be added.
*/
public function getUserInfo(): ?string;
/**
* Retrieve the host component of the URI.
*
* If no host is present this method MUST return a null value.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.2.2.
*
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function getHost(): ?string;
/**
* Retrieve the port component of the URI.
*
* If a port is present, and it is non-standard for the current scheme,
* this method MUST return it as an integer. If the port is the standard port
* used with the current scheme, this method SHOULD return null.
*
* If no port is present, and no scheme is present, this method MUST return
* a null value.
*
* If no port is present, but a scheme is present, this method MAY return
* the standard port for that scheme, but SHOULD return null.
*/
public function getPort(): ?int;
/**
* Retrieve the path component of the URI.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Normally, the empty path "" and absolute path "/" are considered equal as
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
* do this normalization because in contexts with a trimmed base path, e.g.
* the front controller, this difference becomes significant. It's the task
* of the user to handle both "" and "/".
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.3.
*
* As an example, if the value should include a slash ("/") not intended as
* delimiter between path segments, that value MUST be passed in encoded
* form (e.g., "%2F") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.3
*/
public function getPath(): string;
/**
* Retrieve the query string of the URI.
*
* If no host is present this method MUST return a null value.
*
* The leading "?" character is not part of the query and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.4.
*
* As an example, if a value in a key/value pair of the query string should
* include an ampersand ("&") not intended as a delimiter between values,
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.4
*/
public function getQuery(): ?string;
/**
* Retrieve the fragment component of the URI.
*
* If no host is present this method MUST return a null value.
*
* The leading "#" character is not part of the fragment and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.5.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.5
*/
public function getFragment(): ?string;
/**
* Returns an associative array containing all the URI components.
*
* The returned array is similar to PHP's parse_url return value with the following
* differences:
*
* <ul>
* <li>All components are present in the returned array</li>
* <li>Empty and undefined component are treated differently. And empty component is
* set to the empty string while an undefined component is set to the `null` value.</li>
* </ul>
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return ComponentMap
*/
public function getComponents(): array;
/**
* Return an instance with the specified scheme.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified scheme.
*
* A null value provided for the scheme is equivalent to removing the scheme
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withScheme(Stringable|string|null $scheme): self;
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; a null value for the user is equivalent to removing user
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self;
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* A null value provided for the host is equivalent to removing the host
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
* @throws MissingFeature for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(Stringable|string|null $host): self;
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPort(?int $port): self;
/**
* Return an instance with the specified path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified path.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Users can provide both encoded and decoded path characters.
* Implementations ensure the correct encoding as outlined in getPath().
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPath(Stringable|string $path): self;
/**
* Return an instance with the specified query string.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified query string.
*
* Users can provide both encoded and decoded query characters.
* Implementations ensure the correct encoding as outlined in getQuery().
*
* A null value provided for the query is equivalent to removing the query
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withQuery(Stringable|string|null $query): self;
/**
* Return an instance with the specified URI fragment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified URI fragment.
*
* Users can provide both encoded and decoded fragment characters.
* Implementations ensure the correct encoding as outlined in getFragment().
*
* A null value provided for the fragment is equivalent to removing the fragment
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withFragment(Stringable|string|null $fragment): self;
}

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

176
vendor/league/uri-interfaces/Encoder.php vendored Normal file
View File

@@ -0,0 +1,176 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Closure;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use SensitiveParameter;
use Stringable;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function strtoupper;
final class Encoder
{
private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/';
private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},';
private const REGEXP_CHARS_PREVENTS_DECODING = ',%
2[A-F|1-2|4-9]|
3[0-9|B|D]|
4[1-9|A-F]|
5[0-9|A|F]|
6[1-9|A-F]|
7[0-9|E]
,ix';
private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~';
private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})';
/**
* Encode User.
*
* All generic delimiters MUST be encoded
*/
public static function encodeUser(Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
/**
* Encode Password.
*
* Generic delimiters ":" MUST NOT be encoded
*/
public static function encodePassword(#[SensitiveParameter] Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
/**
* Encode Path.
*
* Generic delimiters ":", "@", and "/" MUST NOT be encoded
*/
public static function encodePath(Stringable|string|null $component): string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
return (string) self::encode($component, $pattern);
}
/**
* Encode Query or Fragment.
*
* Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded
*/
public static function encodeQueryOrFragment(Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
public static function encodeQueryKeyValue(mixed $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/';
$encodeMatches = static fn (array $matches): string => match (1) {
preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]),
default => $matches[0],
};
$component = self::filterComponent($component);
return match (true) {
!is_scalar($component) => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))),
1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => rawurlencode($component),
1 === preg_match($pattern, $component) => (string) preg_replace_callback($pattern, $encodeMatches(...), $component),
default => $component,
};
}
/**
* Decodes the URI component without decoding the unreserved characters which are already encoded.
*/
public static function decodePartial(Stringable|string|int|null $component): ?string
{
$decodeMatches = static fn (array $matches): string => match (1) {
preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0]) => strtoupper($matches[0]),
default => rawurldecode($matches[0]),
};
return self::decode($component, $decodeMatches);
}
/**
* Decodes all the URI component characters.
*/
public static function decodeAll(Stringable|string|int|null $component): ?string
{
$decodeMatches = static fn (array $matches): string => rawurldecode($matches[0]);
return self::decode($component, $decodeMatches);
}
private static function filterComponent(mixed $component): ?string
{
return match (true) {
true === $component => '1',
false === $component => '0',
$component instanceof UriComponentInterface => $component->value(),
$component instanceof Stringable,
is_scalar($component) => (string) $component,
null === $component => null,
default => throw new SyntaxError(sprintf('The component must be a scalar value `%s` given.', gettype($component))),
};
}
private static function encode(Stringable|string|int|bool|null $component, string $pattern): ?string
{
$component = self::filterComponent($component);
$encodeMatches = static fn (array $matches): string => match (1) {
preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]),
default => $matches[0],
};
return match (true) {
null === $component,
'' === $component => $component,
default => (string) preg_replace_callback($pattern, $encodeMatches(...), $component),
};
}
/**
* Decodes all the URI component characters.
*/
private static function decode(Stringable|string|int|null $component, Closure $decodeMatches): ?string
{
$component = self::filterComponent($component);
return match (true) {
null === $component => null,
1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => throw new SyntaxError('Invalid component string: '.$component.'.'),
1 === preg_match(self::REGEXP_CHARS_ENCODED, $component) => preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decodeMatches(...), $component),
default => $component,
};
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use League\Uri\Idna\Error;
use League\Uri\Idna\Result;
use Stringable;
final class ConversionFailed extends SyntaxError
{
private function __construct(
string $message,
private readonly string $host,
private readonly Result $result
) {
parent::__construct($message);
}
public static function dueToIdnError(Stringable|string $host, Result $result): self
{
$reasons = array_map(fn (Error $error): string => $error->description(), $result->errors());
return new self('Host `'.$host.'` is invalid: '.implode('; ', $reasons).'.', (string) $host, $result);
}
public function getHost(): string
{
return $this->host;
}
public function getResult(): Result
{
return $this->result;
}
}

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,56 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use finfo;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\IPv4\Calculator;
use const PHP_INT_SIZE;
/**
* Allow detecting features needed to make the packages work.
*/
final class FeatureDetection
{
public static function supportsFileDetection(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? class_exists(finfo::class);
if (!$isSupported) {
throw new MissingFeature('Support for file type detection requires the `fileinfo` extension.');
}
}
public static function supportsIdn(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46'));
if (!$isSupported) {
throw new MissingFeature('Support for IDN host requires the `intl` extension for best performance or run "composer require symfony/polyfill-intl-idn" to install a polyfill.');
}
}
public static function supportsIPv4Conversion(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? (extension_loaded('gmp') || extension_loaded('bcmath') || (4 < PHP_INT_SIZE));
if (!$isSupported) {
throw new MissingFeature('A '.Calculator::class.' implementation could not be automatically loaded. To perform IPv4 conversion use a x.64 PHP build or install one of the following extension GMP or BCMath. You can also ship your own implmentation.');
}
}
}

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, $value2): int
{
return bccomp((string) $value1, (string) $value2, self::SCALE);
}
public function multiply(mixed $value1, $value2): string
{
return bcmul((string) $value1, (string) $value2, self::SCALE);
}
public function div(mixed $value, mixed $base): string
{
return bcdiv((string) $value, (string) $base, self::SCALE);
}
public function mod(mixed $value, mixed $base): string
{
return bcmod((string) $value, (string) $base, self::SCALE);
}
public function add(mixed $value1, mixed $value2): string
{
return bcadd((string) $value1, (string) $value2, self::SCALE);
}
public function sub(mixed $value1, mixed $value2): string
{
return bcsub((string) $value1, (string) $value2, self::SCALE);
}
}

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,303 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\FeatureDetection;
use Stringable;
use function array_pop;
use function count;
use function explode;
use function extension_loaded;
use function ltrim;
use function preg_match;
use function str_ends_with;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Converter
{
private const REGEXP_IPV4_HOST = '/
(?(DEFINE) # . is missing as it is used to separate labels
(?<hexadecimal>0x[[:xdigit:]]*)
(?<octal>0[0-7]*)
(?<decimal>\d+)
(?<ipv4_part>(?:(?&hexadecimal)|(?&octal)|(?&decimal))*)
)
^(?:(?&ipv4_part)\.){0,3}(?&ipv4_part)\.?$
/x';
private const REGEXP_IPV4_NUMBER_PER_BASE = [
'/^0x(?<number>[[:xdigit:]]*)$/' => 16,
'/^0(?<number>[0-7]*)$/' => 8,
'/^(?<number>\d+)$/' => 10,
];
private const IPV6_6TO4_PREFIX = '2002:';
private const IPV4_MAPPED_PREFIX = '::ffff:';
private readonly mixed $maxIPv4Number;
public function __construct(
private readonly Calculator $calculator
) {
$this->maxIPv4Number = $calculator->sub($calculator->pow(2, 32), 1);
}
/**
* Returns an instance using a GMP calculator.
*/
public static function fromGMP(): self
{
return new self(new GMPCalculator());
}
/**
* Returns an instance using a Bcmath calculator.
*/
public static function fromBCMath(): self
{
return new self(new BCMathCalculator());
}
/**
* Returns an instance using a PHP native calculator (requires 64bits PHP).
*/
public static function fromNative(): self
{
return new self(new NativeCalculator());
}
/**
* Returns an instance using a detected calculator depending on the PHP environment.
*
* @throws MissingFeature If no Calculator implementing object can be used on the platform
*
* @codeCoverageIgnore
*/
public static function fromEnvironment(): self
{
FeatureDetection::supportsIPv4Conversion();
return match (true) {
extension_loaded('gmp') => self::fromGMP(),
extension_loaded('bcmath') => self::fromBCMath(),
default => self::fromNative(),
};
}
public function isIpv4(Stringable|string|null $host): bool
{
if (null === $host) {
return false;
}
if (null !== $this->toDecimal($host)) {
return true;
}
$host = (string) $host;
if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return false;
}
$ipAddress = strtolower((string) inet_ntop((string) inet_pton($host)));
if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) {
return false !== filter_var(substr($ipAddress, 7), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}
if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) {
return false;
}
$hexParts = explode(':', substr($ipAddress, 5, 9));
return count($hexParts) > 1
&& false !== long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1]));
}
public function toIPv6Using6to4(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
/** @var array<string> $parts */
$parts = array_map(
fn (string $part): string => sprintf('%02x', $part),
explode('.', $host)
);
return '['.self::IPV6_6TO4_PREFIX.$parts[0].$parts[1].':'.$parts[2].$parts[3].'::]';
}
public function toIPv6UsingMapping(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
return '['.self::IPV4_MAPPED_PREFIX.$host.']';
}
public function toOctal(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
return match (null) {
$host => null,
default => implode('.', array_map(
fn ($value) => str_pad(decoct((int) $value), 4, '0', STR_PAD_LEFT),
explode('.', $host)
)),
};
}
public function toHexadecimal(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
return match (null) {
$host => null,
default => '0x'.implode('', array_map(
fn ($value) => dechex((int) $value),
explode('.', $host)
)),
};
}
/**
* Tries to convert a IPv4 hexadecimal or a IPv4 octal notation into a IPv4 dot-decimal notation if possible
* otherwise returns null.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function toDecimal(Stringable|string|null $host): ?string
{
$host = (string) $host;
if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
$host = substr($host, 1, -1);
if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return null;
}
$ipAddress = strtolower((string) inet_ntop((string) inet_pton($host)));
if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) {
return substr($ipAddress, 7);
}
if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) {
return null;
}
$hexParts = explode(':', substr($ipAddress, 5, 9));
return (string) match (true) {
count($hexParts) < 2 => null,
default => long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1])),
};
}
if (1 !== preg_match(self::REGEXP_IPV4_HOST, $host)) {
return null;
}
if (str_ends_with($host, '.')) {
$host = substr($host, 0, -1);
}
$numbers = [];
foreach (explode('.', $host) as $label) {
$number = $this->labelToNumber($label);
if (null === $number) {
return null;
}
$numbers[] = $number;
}
$ipv4 = array_pop($numbers);
$max = $this->calculator->pow(256, 6 - count($numbers));
if ($this->calculator->compare($ipv4, $max) > 0) {
return null;
}
foreach ($numbers as $offset => $number) {
if ($this->calculator->compare($number, 255) > 0) {
return null;
}
$ipv4 = $this->calculator->add($ipv4, $this->calculator->multiply(
$number,
$this->calculator->pow(256, 3 - $offset)
));
}
return $this->long2Ip($ipv4);
}
/**
* Converts a domain label into a IPv4 integer part.
*
* @see https://url.spec.whatwg.org/#ipv4-number-parser
*
* @return mixed returns null if it cannot correctly convert the label
*/
private function labelToNumber(string $label): mixed
{
foreach (self::REGEXP_IPV4_NUMBER_PER_BASE as $regexp => $base) {
if (1 !== preg_match($regexp, $label, $matches)) {
continue;
}
$number = ltrim($matches['number'], '0');
if ('' === $number) {
return 0;
}
$number = $this->calculator->baseConvert($number, $base);
if (0 <= $this->calculator->compare($number, 0) && 0 >= $this->calculator->compare($number, $this->maxIPv4Number)) {
return $number;
}
}
return null;
}
/**
* Generates the dot-decimal notation for IPv4.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*
* @param mixed $ipAddress the number representation of the IPV4address
*/
private function long2Ip(mixed $ipAddress): string
{
$output = '';
for ($offset = 0; $offset < 4; $offset++) {
$output = $this->calculator->mod($ipAddress, 256).$output;
if ($offset < 3) {
$output = '.'.$output;
}
$ipAddress = $this->calculator->div($ipAddress, 256);
}
return $output;
}
}

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,137 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv6;
use Stringable;
use ValueError;
use function filter_var;
use function implode;
use function inet_pton;
use function str_split;
use function strtolower;
use function unpack;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Converter
{
/**
* Significant 10 bits of IP to detect Zone ID regular expression pattern.
*
* @var string
*/
private const HOST_ADDRESS_BLOCK = "\xfe\x80";
public static function compressIp(string $ipAddress): string
{
return match (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
false => throw new ValueError('The submitted IP is not a valid IPv6 address.'),
default => strtolower((string) inet_ntop((string) inet_pton($ipAddress))),
};
}
public static function expandIp(string $ipAddress): string
{
if (false === filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
throw new ValueError('The submitted IP is not a valid IPv6 address.');
}
$hex = (array) unpack('H*hex', (string) inet_pton($ipAddress));
return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));
}
public static function compress(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}
$components['ipAddress'] = self::compressIp($components['ipAddress']);
return self::build($components);
}
public static function expand(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}
$components['ipAddress'] = self::expandIp($components['ipAddress']);
return self::build($components);
}
private static function build(array $components): string
{
$components['ipAddress'] ??= null;
$components['zoneIdentifier'] ??= null;
if (null === $components['ipAddress']) {
return '';
}
return '['.$components['ipAddress'].match ($components['zoneIdentifier']) {
null => '',
default => '%'.$components['zoneIdentifier'],
}.']';
}
/**]
* @param Stringable|string|null $host
*
* @return array{ipAddress:string|null, zoneIdentifier:string|null}
*/
private static function parse(Stringable|string|null $host): array
{
if (null === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
$host = (string) $host;
if ('' === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if (!str_starts_with($host, '[')) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if (!str_ends_with($host, ']')) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
[$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null];
if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
return match (true) {
null === $zoneIdentifier,
is_string($ipv6) && str_starts_with((string)inet_pton($ipv6), self::HOST_ADDRESS_BLOCK) => ['ipAddress' => $ipv6, 'zoneIdentifier' => $zoneIdentifier],
default => ['ipAddress' => null, 'zoneIdentifier' => null],
};
}
}

View File

@@ -0,0 +1,218 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Idna;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\FeatureDetection;
use Stringable;
use function idn_to_ascii;
use function idn_to_utf8;
use function rawurldecode;
use const INTL_IDNA_VARIANT_UTS46;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Converter
{
private const REGEXP_IDNA_PATTERN = '/[^\x20-\x7f]/';
private const MAX_DOMAIN_LENGTH = 253;
private const MAX_LABEL_LENGTH = 63;
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&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(Stringable|string $domain, Option|int|null $options = null): string
{
$result = self::toAscii($domain, $options);
return match (true) {
$result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result),
default => $result->domain(),
};
}
/**
* Converts the input to its IDNA ASCII form.
*
* This method returns the string converted to IDN ASCII form
*
* @throws SyntaxError if the string cannot be converted to ASCII using IDN UTS46 algorithm
*/
public static function toAscii(Stringable|string $domain, Option|int|null $options = null): Result
{
$domain = rawurldecode((string) $domain);
if (1 === preg_match(self::REGEXP_IDNA_PATTERN, $domain)) {
FeatureDetection::supportsIdn();
$flags = match (true) {
null === $options => Option::forIDNA2008Ascii(),
$options instanceof Option => $options,
default => Option::new($options),
};
idn_to_ascii($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return Result::fromIntl([
'result' => strtolower($domain),
'isTransitionalDifferent' => false,
'errors' => self::validateDomainAndLabelLength($domain),
]);
}
return Result::fromIntl($idnaInfo);
}
$error = Error::NONE->value;
if (1 !== preg_match(self::REGEXP_REGISTERED_NAME, $domain)) {
$error |= Error::DISALLOWED->value;
}
return Result::fromIntl([
'result' => strtolower($domain),
'isTransitionalDifferent' => false,
'errors' => self::validateDomainAndLabelLength($domain) | $error,
]);
}
/**
* Converts the input to its IDNA UNICODE form or throw on failure.
*
* @see Converter::toUnicode()
*
* @throws ConversionFailed if the conversion returns error
*/
public static function toUnicodeOrFail(Stringable|string $domain, Option|int|null $options = null): string
{
$result = self::toUnicode($domain, $options);
return match (true) {
$result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result),
default => $result->domain(),
};
}
/**
* Converts the input to its IDNA UNICODE form.
*
* This method returns the string converted to IDN UNICODE form
*
* @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm
*/
public static function toUnicode(Stringable|string $domain, Option|int|null $options = null): Result
{
$domain = rawurldecode((string) $domain);
if (false === stripos($domain, 'xn--')) {
return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]);
}
FeatureDetection::supportsIdn();
$flags = match (true) {
null === $options => Option::forIDNA2008Unicode(),
$options instanceof Option => $options,
default => Option::new($options),
};
idn_to_utf8($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]);
}
return Result::fromIntl($idnaInfo);
}
/**
* Tells whether the submitted host is a valid IDN regardless of its format.
*
* Returns false if the host is invalid or if its conversion yield the same result
*/
public static function isIdn(Stringable|string|null $domain): bool
{
$domain = strtolower(rawurldecode((string) $domain));
$result = match (1) {
preg_match(self::REGEXP_IDNA_PATTERN, $domain) => self::toAscii($domain),
default => self::toUnicode($domain),
};
return match (true) {
$result->hasErrors() => false,
default => $result->domain() !== $domain,
};
}
/**
* Adapted from https://github.com/TRowbotham/idna.
*
* @see https://github.com/TRowbotham/idna/blob/master/src/Idna.php#L236
*/
private static function validateDomainAndLabelLength(string $domain): int
{
$error = Error::NONE->value;
$labels = explode('.', $domain);
$maxDomainSize = self::MAX_DOMAIN_LENGTH;
$length = count($labels);
// If the last label is empty, and it is not the first label, then it is the root label.
// Increase the max size by 1, making it 254, to account for the root label's "."
// delimiter. This also means we don't need to check the last label's length for being too
// long.
if ($length > 1 && '' === $labels[$length - 1]) {
++$maxDomainSize;
array_pop($labels);
}
if (strlen($domain) > $maxDomainSize) {
$error |= Error::DOMAIN_NAME_TOO_LONG->value;
}
foreach ($labels as $label) {
if (strlen($label) > self::MAX_LABEL_LENGTH) {
$error |= Error::LABEL_TOO_LONG->value;
break;
}
}
return $error;
}
}

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

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

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,209 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\KeyValuePair;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use function array_combine;
use function explode;
use function implode;
use function is_float;
use function is_int;
use function is_string;
use function json_encode;
use function preg_match;
use function str_replace;
use const JSON_PRESERVE_ZERO_FRACTION;
use const PHP_QUERY_RFC1738;
use const PHP_QUERY_RFC3986;
final class Converter
{
private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
/**
* @param non-empty-string $separator the query string separator
* @param array<string> $fromRfc3986 contains all the RFC3986 encoded characters to be converted
* @param array<string> $toEncoding contains all the expected encoded characters
*/
private function __construct(
private readonly string $separator,
private readonly array $fromRfc3986 = [],
private readonly array $toEncoding = [],
) {
if ('' === $this->separator) {
throw new SyntaxError('The separator character must be a non empty string.');
}
}
/**
* @param non-empty-string $separator
*/
public static function new(string $separator): self
{
return new self($separator);
}
/**
* @param non-empty-string $separator
*/
public static function fromRFC3986(string $separator = '&'): self
{
return self::new($separator);
}
/**
* @param non-empty-string $separator
*/
public static function fromRFC1738(string $separator = '&'): self
{
return self::new($separator)
->withEncodingMap(['%20' => '+']);
}
/**
* @param non-empty-string $separator
*
* @see https://url.spec.whatwg.org/#application/x-www-form-urlencoded
*/
public static function fromFormData(string $separator = '&'): self
{
return self::new($separator)
->withEncodingMap(['%20' => '+', '%2A' => '*']);
}
public static function fromEncodingType(int $encType): self
{
return match ($encType) {
PHP_QUERY_RFC3986 => self::fromRFC3986(),
PHP_QUERY_RFC1738 => self::fromRFC1738(),
default => throw new SyntaxError('Unknown or Unsupported encoding.'),
};
}
/**
* @return non-empty-string
*/
public function separator(): string
{
return $this->separator;
}
/**
* @return array<string, string>
*/
public function encodingMap(): array
{
return array_combine($this->fromRfc3986, $this->toEncoding);
}
/**
* @return array<non-empty-list<string|null>>
*/
public function toPairs(Stringable|string|int|float|bool|null $value): array
{
$value = match (true) {
$value instanceof UriComponentInterface => $value->value(),
$value instanceof Stringable, is_int($value) => (string) $value,
false === $value => '0',
true === $value => '1',
default => $value,
};
if (null === $value) {
return [];
}
$value = match (1) {
preg_match(self::REGEXP_INVALID_CHARS, (string) $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'),
default => str_replace($this->toEncoding, $this->fromRfc3986, (string) $value),
};
return array_map(
fn (string $pair): array => explode('=', $pair, 2) + [1 => null],
explode($this->separator, $value)
);
}
private static function vString(Stringable|string|bool|int|float|null $value): ?string
{
return match (true) {
$value => '1',
false === $value => '0',
null === $value => null,
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
/**
* @param iterable<array{0:string|null, 1:Stringable|string|bool|int|float|null}> $pairs
*/
public function toValue(iterable $pairs): ?string
{
$filteredPairs = [];
foreach ($pairs as $pair) {
$filteredPairs[] = match (true) {
!is_string($pair[0]) => throw new SyntaxError('the pair key MUST be a string;, `'.gettype($pair[0]).'` given.'),
null === $pair[1] => self::vString($pair[0]),
default => self::vString($pair[0]).'='.self::vString($pair[1]),
};
}
return match ([]) {
$filteredPairs => null,
default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)),
};
}
/**
* @param non-empty-string $separator
*/
public function withSeparator(string $separator): self
{
return match ($this->separator) {
$separator => $this,
default => new self($separator, $this->fromRfc3986, $this->toEncoding),
};
}
/**
* Sets the conversion map.
*
* Each key from the iterable structure represents the RFC3986 encoded characters as string,
* while each value represents the expected output encoded characters
*/
public function withEncodingMap(iterable $encodingMap): self
{
$fromRfc3986 = [];
$toEncoding = [];
foreach ($encodingMap as $from => $to) {
[$fromRfc3986[], $toEncoding[]] = match (true) {
!is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'),
$to instanceof Stringable,
is_string($to) => [$from, (string) $to],
default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'),
};
}
return match (true) {
$fromRfc3986 !== $this->fromRfc3986,
$toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding),
default => $this,
};
}
}

20
vendor/league/uri-interfaces/LICENSE vendored Normal 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,276 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use Stringable;
use function array_key_exists;
use function array_keys;
use function is_array;
use function rawurldecode;
use function strpos;
use function substr;
use const PHP_QUERY_RFC3986;
/**
* A class to parse the URI query string.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.4
*/
final class QueryString
{
private const PAIR_VALUE_DECODED = 1;
private const PAIR_VALUE_PRESERVED = 2;
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Build a query string from a list of pairs.
*
* @see QueryString::buildFromPairs()
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*
* @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs
* @param non-empty-string $separator
*
* @throws SyntaxError If the encoding type is invalid
* @throws SyntaxError If a pair is invalid
*/
public static function build(iterable $pairs, string $separator = '&', int $encType = PHP_QUERY_RFC3986): ?string
{
return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator));
}
/**
* Build a query string from a list of pairs.
*
* The method expects the return value from Query::parse to build
* a valid query string. This method differs from PHP http_build_query as
* it does not modify parameters keys.
*
* If a reserved character is found in a URI component and
* no delimiting role is known for that character, then it must be
* interpreted as representing the data octet corresponding to that
* character's encoding in US-ASCII.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*
* @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs
*
* @throws SyntaxError If the encoding type is invalid
* @throws SyntaxError If a pair is invalid
*/
public static function buildFromPairs(iterable $pairs, ?Converter $converter = null): ?string
{
$keyValuePairs = [];
foreach ($pairs as $pair) {
if (!is_array($pair) || [0, 1] !== array_keys($pair)) {
throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.');
}
$keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($pair[0]), match(null) {
$pair[1] => null,
default => Encoder::encodeQueryKeyValue($pair[1]),
}];
}
return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs);
}
/**
* Parses the query string like parse_str without mangling the results.
*
* @see QueryString::extractFromValue()
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @param non-empty-string $separator
*
* @throws SyntaxError
*/
public static function extract(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array
{
return self::extractFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator));
}
/**
* Parses the query string like parse_str without mangling the results.
*
* The result is similar as PHP parse_str when used with its
* second argument with the difference that variable names are
* not mangled.
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @throws SyntaxError
*/
public static function extractFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array
{
return self::convert(self::decodePairs(
($converter ?? Converter::fromRFC3986())->toPairs($query),
self::PAIR_VALUE_PRESERVED
));
}
/**
* Parses a query string into a collection of key/value pairs.
*
* @param non-empty-string $separator
*
* @throws SyntaxError
*
* @return array<int, array{0:string, 1:string|null}>
*/
public static function parse(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array
{
return self::parseFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator));
}
/**
* Parses a query string into a collection of key/value pairs.
*
* @throws SyntaxError
*
* @return array<int, array{0:string, 1:string|null}>
*/
public static function parseFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array
{
return self::decodePairs(
($converter ?? Converter::fromRFC3986())->toPairs($query),
self::PAIR_VALUE_DECODED
);
}
/**
* @param array<non-empty-list<string|null>> $pairs
*
* @return array<int, array{0:string, 1:string|null}>
*/
private static function decodePairs(array $pairs, int $pairValueState): array
{
$decodePair = static function (array $pair, int $pairValueState): array {
[$key, $value] = $pair;
return match ($pairValueState) {
self::PAIR_VALUE_PRESERVED => [(string) Encoder::decodeAll($key), $value],
default => [(string) Encoder::decodeAll($key), Encoder::decodeAll($value)],
};
};
return array_reduce(
$pairs,
fn (array $carry, array $pair) => [...$carry, $decodePair($pair, $pairValueState)],
[]
);
}
/**
* Converts a collection of key/value pairs and returns
* the store PHP variables as elements of an array.
*/
public static function convert(iterable $pairs): array
{
$returnedValue = [];
foreach ($pairs as $pair) {
$returnedValue = self::extractPhpVariable($returnedValue, $pair);
}
return $returnedValue;
}
/**
* Parses a query pair like parse_str without mangling the results array keys.
*
* <ul>
* <li>empty name are not saved</li>
* <li>If the value from name is duplicated its corresponding value will be overwritten</li>
* <li>if no "[" is detected the value is added to the return array with the name as index</li>
* <li>if no "]" is detected after detecting a "[" the value is added to the return array with the name as index</li>
* <li>if there's a mismatch in bracket usage the remaining part is dropped</li>
* <li>“.” and “ ” are not converted to “_”</li>
* <li>If there is no “]”, then the first “[” is not converted to becomes an “_”</li>
* <li>no whitespace trimming is done on the key value</li>
* </ul>
*
* @see https://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic1.phpt
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic2.phpt
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic3.phpt
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic4.phpt
*
* @param array $data the submitted array
* @param array|string $name the pair key
* @param string $value the pair value
*/
private static function extractPhpVariable(array $data, array|string $name, string $value = ''): array
{
if (is_array($name)) {
[$name, $value] = $name;
$value = rawurldecode((string) $value);
}
if ('' === $name) {
return $data;
}
$leftBracketPosition = strpos($name, '[');
if (false === $leftBracketPosition) {
$data[$name] = $value;
return $data;
}
$rightBracketPosition = strpos($name, ']', $leftBracketPosition);
if (false === $rightBracketPosition) {
$data[$name] = $value;
return $data;
}
$key = substr($name, 0, $leftBracketPosition);
if ('' === $key) {
$key = '0';
}
if (!array_key_exists($key, $data) || !is_array($data[$key])) {
$data[$key] = [];
}
$remaining = substr($name, $rightBracketPosition + 1);
if (!str_starts_with($remaining, '[') || !str_contains($remaining, ']')) {
$remaining = '';
}
$name = substr($name, $leftBracketPosition + 1, $rightBracketPosition - $leftBracketPosition - 1).$remaining;
if ('' === $name) {
$data[$key][] = $value;
return $data;
}
$data[$key] = self::extractPhpVariable($data[$key], $name, $value);
return $data;
}
}

View File

@@ -0,0 +1,513 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter;
use Stringable;
use function array_merge;
use function explode;
use function filter_var;
use function inet_pton;
use function preg_match;
use function rawurldecode;
use function sprintf;
use function strpos;
use function substr;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* A class to parse a URI string according to RFC3986.
*
* @link https://tools.ietf.org/html/rfc3986
* @package League\Uri
* @author Ignace Nyamagana Butera <nyamsprod@gmail.com>
* @since 6.0.0
*
* @phpstan-type AuthorityMap array{user:?string, pass:?string, host:?string, port:?int}
* @phpstan-type ComponentMap array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string}
* @phpstan-type InputComponentMap array{scheme? : ?string, user? : ?string, pass? : ?string, host? : ?string, port? : ?int, path? : ?string, query? : ?string, fragment? : ?string}
*/
final class UriString
{
/**
* Default URI component values.
*
* @var ComponentMap
*/
private const URI_COMPONENTS = [
'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
];
/**
* Simple URI which do not need any parsing.
*
* @var array<string, array<string>>
*/
private const URI_SHORTCUTS = [
'' => [],
'#' => ['fragment' => ''],
'?' => ['query' => ''],
'?#' => ['query' => '', 'fragment' => ''],
'/' => ['path' => '/'],
'//' => ['host' => ''],
];
/**
* Range of invalid characters in URI string.
*
* @var string
*/
private const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
/**
* RFC3986 regular expression URI splitter.
*
* @link https://tools.ietf.org/html/rfc3986#appendix-B
* @var string
*/
private const REGEXP_URI_PARTS = ',^
(?<scheme>(?<scontent>[^:/?\#]+):)? # URI scheme component
(?<authority>//(?<acontent>[^/?\#]*))? # URI authority part
(?<path>[^?\#]*) # URI path component
(?<query>\?(?<qcontent>[^\#]*))? # URI query component
(?<fragment>\#(?<fcontent>.*))? # URI fragment component
,x';
/**
* URI scheme regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.1
* @var string
*/
private const REGEXP_URI_SCHEME = '/^([a-z][a-z\d+.-]*)?$/i';
/**
* IPvFuture regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @var string
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F0-9])+\.
(?:
(?<unreserved>[a-z0-9_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
/**
* General registered name regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @var string
*/
private const REGEXP_REGISTERED_NAME = '/(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$/ix';
/**
* Invalid characters in host regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @var string
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* Invalid path for URI without scheme and authority regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.3
* @var string
*/
private const REGEXP_INVALID_PATH = ',^(([^/]*):)(.*)?/,';
/**
* Host and Port splitter regular expression.
*
* @var string
*/
private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,';
/**
* IDN Host detector regular expression.
*
* @var string
*/
private const REGEXP_IDN_PATTERN = '/[^\x20-\x7f]/';
/**
* Only the address block fe80::/10 can have a Zone ID attach to
* let's detect the link local significant 10 bits.
*
* @var string
*/
private const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80";
/**
* Maximum number of host cached.
*
* @var int
*/
private const MAXIMUM_HOST_CACHED = 100;
/**
* Generate a URI string representation from its parsed representation
* returned by League\UriString::parse() or PHP's parse_url.
*
* If you supply your own array, you are responsible for providing
* valid components without their URI delimiters.
*
* @link https://tools.ietf.org/html/rfc3986#section-5.3
* @link https://tools.ietf.org/html/rfc3986#section-7.5
*
* @param InputComponentMap $components
*/
public static function build(array $components): string
{
return self::buildUri(
$components['scheme'] ?? null,
self::buildAuthority($components),
$components['path'] ?? '',
$components['query'] ?? null,
$components['fragment'] ?? null,
);
}
/**
* Generate a URI string representation based on RFC3986 algorithm.
*
* valid URI component MUST be provided without their URI delimiters
* but properly encoded.
*
* @link https://tools.ietf.org/html/rfc3986#section-5.3
* @link https://tools.ietf.org/html/rfc3986#section-7.5
*/
public static function buildUri(
?string $scheme,
?string $authority,
string $path,
?string $query,
?string $fragment,
): string {
$uri = '';
if (null !== $scheme) {
$uri .= $scheme.':';
}
if (null !== $authority) {
$uri .= '//'.$authority;
}
$uri .= $path;
if (null !== $query) {
$uri .= '?'.$query;
}
if (null !== $fragment) {
$uri .= '#'.$fragment;
}
return $uri;
}
/**
* Generate a URI authority representation from its parsed representation.
*
* @param InputComponentMap $components
*/
public static function buildAuthority(array $components): ?string
{
if (!isset($components['host'])) {
return null;
}
$authority = $components['host'];
if (isset($components['port'])) {
$authority .= ':'.$components['port'];
}
if (!isset($components['user'])) {
return $authority;
}
$authority = '@'.$authority;
if (!isset($components['pass'])) {
return $components['user'].$authority;
}
return $components['user'].':'.$components['pass'].$authority;
}
/**
* Parse a URI string into its components.
*
* This method parses a URI and returns an associative array containing any
* of the various components of the URI that are present.
*
* <code>
* $components = UriString::parse('http://foo@test.example.com:42?query#');
* var_export($components);
* //will display
* array(
* 'scheme' => 'http', // the URI scheme component
* 'user' => 'foo', // the URI user component
* 'pass' => null, // the URI pass component
* 'host' => 'test.example.com', // the URI host component
* 'port' => 42, // the URI port component
* 'path' => '', // the URI path component
* 'query' => 'query', // the URI query component
* 'fragment' => '', // the URI fragment component
* );
* </code>
*
* The returned array is similar to PHP's parse_url return value with the following
* differences:
*
* <ul>
* <li>All components are always present in the returned array</li>
* <li>Empty and undefined component are treated differently. And empty component is
* set to the empty string while an undefined component is set to the `null` value.</li>
* <li>The path component is never undefined</li>
* <li>The method parses the URI following the RFC3986 rules, but you are still
* required to validate the returned components against its related scheme specific rules.</li>
* </ul>
*
* @link https://tools.ietf.org/html/rfc3986
*
* @throws SyntaxError if the URI contains invalid characters
* @throws SyntaxError if the URI contains an invalid scheme
* @throws SyntaxError if the URI contains an invalid path
*
* @return ComponentMap
*/
public static function parse(Stringable|string|int $uri): array
{
$uri = (string) $uri;
if (isset(self::URI_SHORTCUTS[$uri])) {
/** @var ComponentMap $components */
$components = array_merge(self::URI_COMPONENTS, self::URI_SHORTCUTS[$uri]);
return $components;
}
if (1 === preg_match(self::REGEXP_INVALID_URI_CHARS, $uri)) {
throw new SyntaxError(sprintf('The uri `%s` contains invalid characters', $uri));
}
//if the first character is a known URI delimiter parsing can be simplified
$first_char = $uri[0];
//The URI is made of the fragment only
if ('#' === $first_char) {
[, $fragment] = explode('#', $uri, 2);
$components = self::URI_COMPONENTS;
$components['fragment'] = $fragment;
return $components;
}
//The URI is made of the query and fragment
if ('?' === $first_char) {
[, $partial] = explode('?', $uri, 2);
[$query, $fragment] = explode('#', $partial, 2) + [1 => null];
$components = self::URI_COMPONENTS;
$components['query'] = $query;
$components['fragment'] = $fragment;
return $components;
}
//use RFC3986 URI regexp to split the URI
preg_match(self::REGEXP_URI_PARTS, $uri, $parts);
$parts += ['query' => '', 'fragment' => ''];
if (':' === ($parts['scheme'] ?? null) || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'] ?? '')) {
throw new SyntaxError(sprintf('The uri `%s` contains an invalid scheme', $uri));
}
if ('' === ($parts['scheme'] ?? '').($parts['authority'] ?? '') && 1 === preg_match(self::REGEXP_INVALID_PATH, $parts['path'] ?? '')) {
throw new SyntaxError(sprintf('The uri `%s` contains an invalid path.', $uri));
}
/** @var ComponentMap $components */
$components = array_merge(
self::URI_COMPONENTS,
'' === ($parts['authority'] ?? null) ? [] : self::parseAuthority($parts['acontent'] ?? null),
[
'path' => $parts['path'] ?? '',
'scheme' => '' === ($parts['scheme'] ?? null) ? null : ($parts['scontent'] ?? null),
'query' => '' === $parts['query'] ? null : ($parts['qcontent'] ?? null),
'fragment' => '' === $parts['fragment'] ? null : ($parts['fcontent'] ?? null),
]
);
return $components;
}
/**
* Parses the URI authority part.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2
*
* @throws SyntaxError If the port component is invalid
*
* @return AuthorityMap
*/
public static function parseAuthority(Stringable|string|null $authority): array
{
$components = ['user' => null, 'pass' => null, 'host' => null, 'port' => null];
if (null === $authority) {
return $components;
}
$authority = (string) $authority;
$components['host'] = '';
if ('' === $authority) {
return $components;
}
$parts = explode('@', $authority, 2);
if (isset($parts[1])) {
[$components['user'], $components['pass']] = explode(':', $parts[0], 2) + [1 => null];
}
preg_match(self::REGEXP_HOST_PORT, $parts[1] ?? $parts[0], $matches);
$matches += ['port' => ''];
$components['port'] = self::filterPort($matches['port']);
$components['host'] = self::filterHost($matches['host'] ?? '');
return $components;
}
/**
* Filter and format the port component.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
*/
private static function filterPort(string $port): ?int
{
return match (true) {
'' === $port => null,
1 === preg_match('/^\d*$/', $port) => (int) $port,
default => throw new SyntaxError(sprintf('The port `%s` is invalid', $port)),
};
}
/**
* Returns whether a hostname is valid.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
*/
private static function filterHost(string $host): string
{
if ('' === $host) {
return $host;
}
/** @var array<string, 1> $hostCache */
static $hostCache = [];
if (isset($hostCache[$host])) {
return $host;
}
if (self::MAXIMUM_HOST_CACHED < count($hostCache)) {
array_shift($hostCache);
}
if ('[' !== $host[0] || !str_ends_with($host, ']')) {
self::filterRegisteredName($host);
$hostCache[$host] = 1;
return $host;
}
if (self::isIpHost(substr($host, 1, -1))) {
$hostCache[$host] = 1;
return $host;
}
throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
}
/**
* Throws if the host is not a registered name and not a valid IDN host.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
* @throws MissingFeature if IDN support or ICU requirement are not available or met.
* @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form
*/
private static function filterRegisteredName(string $host): void
{
$formattedHost = rawurldecode($host);
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formattedHost)) {
return;
}
//to test IDN host non-ascii characters must be present in the host
if (1 !== preg_match(self::REGEXP_IDN_PATTERN, $formattedHost)) {
throw new SyntaxError(sprintf('Host `%s` is invalid: the host is not a valid registered name', $host));
}
Converter::toAsciiOrFail($host);
}
/**
* Validates a IPv6/IPfuture host.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @link https://tools.ietf.org/html/rfc6874#section-2
* @link https://tools.ietf.org/html/rfc6874#section-4
*/
private static function isIpHost(string $ipHost): bool
{
if (false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return true;
}
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
return !in_array($matches['version'], ['4', '6'], true);
}
$pos = strpos($ipHost, '%');
if (false === $pos || 1 === preg_match(self::REGEXP_INVALID_HOST_CHARS, rawurldecode(substr($ipHost, $pos)))) {
return false;
}
$ipHost = substr($ipHost, 0, $pos);
return false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton($ipHost), self::ZONE_ID_ADDRESS_BLOCK);
}
}

View File

@@ -0,0 +1,70 @@
{
"name": "league/uri-interfaces",
"type": "library",
"description" : "Common interfaces and classes for URI representation and interaction",
"keywords": [
"url",
"uri",
"rfc3986",
"rfc3987",
"rfc6570",
"psr-7",
"parse_url",
"http",
"https",
"ws",
"ftp",
"data-uri",
"file-uri",
"parse_str",
"query-string",
"querystring",
"hostname"
],
"license": "MIT",
"homepage": "https://uri.thephpleague.com",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://nyamsprod.com"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php" : "^8.1",
"ext-filter": "*",
"psr/http-message": "^1.1 || ^2.0",
"psr/http-factory": "^1"
},
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"php-64bit": "to improve IPV4 host parsing",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"support": {
"forum": "https://thephpleague.slack.com",
"docs": "https://uri.thephpleague.com",
"issues": "https://github.com/thephpleague/uri-src/issues"
},
"config": {
"sort-packages": true
}
}