Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 08:02:04 +01:00
parent 4413528994
commit 1d90fbf296
6865 changed files with 1091082 additions and 0 deletions

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2016 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,68 @@
{
"name": "league/uri-interfaces",
"description" : "Common interface for URI representation",
"keywords": [
"url",
"uri",
"rfc3986",
"rfc3987"
],
"license": "MIT",
"homepage": "http://github.com/thephpleague/uri-interfaces",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://nyamsprod.com"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php" : "^7.2 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.19",
"phpstan/phpstan": "^0.12.90",
"phpstan/phpstan-strict-rules": "^0.12.9",
"phpstan/phpstan-phpunit": "^0.12.19",
"phpunit/phpunit": "^8.5.15 || ^9.5"
},
"autoload": {
"psr-4": {
"League\\Uri\\": "src/"
}
},
"scripts": {
"phpunit": "phpunit --coverage-text",
"phpcs": "php-cs-fixer fix --dry-run --diff -vvv --allow-risky=yes --ansi",
"phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi",
"phpstan": "phpstan analyse -l max -c phpstan.neon src --ansi --memory-limit 192M",
"test": [
"@phpunit",
"@phpstan",
"@phpcs:fix"
]
},
"scripts-descriptions": {
"phpunit": "Runs package test suite",
"phpstan": "Runs complete codebase static analysis",
"phpcs": "Runs coding style testing",
"phpcs:fix": "Fix coding style issues",
"test": "Runs all tests"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"suggest": {
"ext-intl": "to use the IDNA feature",
"symfony/intl": "to use the IDNA feature via Symfony Polyfill"
}
}

View file

@ -0,0 +1,87 @@
<?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\IdnSupportMissing;
use League\Uri\Exceptions\SyntaxError;
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;
/**
* 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.
*
* @param ?string $host
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
* @throws IdnSupportMissing for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(?string $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.
*
* @param ?int $port
*
* @throws SyntaxError for invalid component or transformations
* that would result in a 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.
*
* @param ?string $user
* @param ?string $password
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withUserInfo(?string $user, ?string $password = null): self;
}

View file

@ -0,0 +1,92 @@
<?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 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
* a 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(string $parameters): self;
}

View file

@ -0,0 +1,107 @@
<?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;
/**
* @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.
*
* @param ?string $label
*
* @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(string $label): self;
/**
* Appends a label to the host.
*/
public function append(string $label): 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, 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.
*
* @param int ...$keys
*
* @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,51 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\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;
}

View file

@ -0,0 +1,48 @@
<?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
{
/**
* Returns whether or not the host is an IPv4 address.
*/
public function isIpv4(): bool;
/**
* Returns whether or not the host is an IPv6 address.
*/
public function isIpv6(): bool;
/**
* Returns whether or not the host is an IPv6 address.
*/
public function isIpFuture(): bool;
/**
* Returns whether or not the host has a ZoneIdentifier.
*
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
public function hasZoneIdentifier(): bool;
/**
* Returns an 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;
/**
* Returns whether or not the path is absolute or relative.
*/
public function isAbsolute(): bool;
/**
* Returns whether or not the path has a trailing delimiter.
*/
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,227 @@
<?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;
/**
* @extends \IteratorAggregate<array{0:string, 1:string|null}>
*/
interface QueryInterface extends \Countable, \IteratorAggregate, UriComponentInterface
{
/**
* Returns the query separator.
*/
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 a 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 pair with a specific name exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function has(string $key): 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.
*
* If a key is submitted it will returns the value attached to it or null
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @param ?string $key
* @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 params(?string $key = null);
/**
* Returns the RFC1738 encoded query.
*/
public function toRFC1738(): ?string;
/**
* Returns the RFC3986 encoded query.
*
* @see ::getContent
*/
public function toRFC3986(): ?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;
/**
* 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.
*
* ie.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar
*/
public function withoutNumericIndices(): self;
/**
* Returns an instance with the 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
*
* @param ?string $value
*/
public function withPair(string $key, ?string $value): 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(string $query): self;
/**
* 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
*
* @param string ...$keys
*/
public function withoutPair(string ...$keys): 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
*
* @param ?string $value
*/
public function appendTo(string $key, ?string $value): 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(string $query): 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.
*
* @param string ...$keys
*/
public function withoutParam(string ...$keys): self;
}

View file

@ -0,0 +1,147 @@
<?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;
/**
* @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
*
* @param ?string $segment
*
* @return int[]
*/
public function keys(?string $segment = null): array;
/**
* Appends a segment to the path.
*/
public function append(string $segment): self;
/**
* Prepends a segment to the path.
*/
public function prepend(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.
*
* @param ?string $segment
*
* @throws SyntaxError If the key is invalid
*/
public function withSegment(int $key, ?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.
*
* @param int ...$keys remaining keys to remove
*
* @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.
*
* @param ?string $path
*/
public function withDirname(?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.
*
* @param ?string $basename
*/
public function withBasename(?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.
*
* @param ?string $extension
*/
public function withExtension(?string $extension): self;
}

View file

@ -0,0 +1,88 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use League\Uri\Exceptions\IdnSupportMissing;
use League\Uri\Exceptions\SyntaxError;
interface UriComponentInterface extends \JsonSerializable
{
/**
* Returns the instance content.
*
* If the instance is defined, the value returned MUST be encoded according to the
* selected encoding algorithm. In any case, the value MUST NOT double-encode any character
* depending on the selected encoding algorithm.
*
* To determine what characters to encode, please refer to RFC 3986, Sections 2 and 3.
* or RFC 3987 Section 3. By default the content is encoded according to RFC3986
*
* If the instance is not defined null is returned
*/
public function getContent(): ?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;
/**
* Returns an instance with the specified content.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified content.
*
* Users can provide both encoded and decoded content characters.
*
* A null value is equivalent to removing the component content.
*
*
* @param ?string $content
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
* @throws IdnSupportMissing for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withContent(?string $content): self;
}

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,292 @@
<?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\IdnSupportMissing;
use League\Uri\Exceptions\SyntaxError;
interface UriInterface extends \JsonSerializable
{
/**
* 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;
/**
* 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.
*
* @param ?string $scheme
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withScheme(?string $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.
*
* @param ?string $user
* @param ?string $password
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withUserInfo(?string $user, ?string $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.
*
* @param ?string $host
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
* @throws IdnSupportMissing for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(?string $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.
*
* @param ?int $port
*
* @throws SyntaxError for invalid component or transformations
* that would result in a 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 a object in invalid state.
*/
public function withPath(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.
*
* @param ?string $query
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withQuery(?string $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.
*
* @param ?string $fragment
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withFragment(?string $fragment): self;
}

View file

@ -0,0 +1,40 @@
<?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 UserInfoInterface extends UriComponentInterface
{
/**
* Returns the user component part.
*/
public function getUser(): ?string;
/**
* Returns the pass component part.
*/
public function getPass(): ?string;
/**
* 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 user.
*
* An empty user is equivalent to removing the user information.
*
* @param ?string $user
* @param ?string $pass
*/
public function withUserInfo(?string $user, ?string $pass = null): self;
}

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\Exceptions;
use League\Uri\Contracts\UriException;
class FileinfoSupportMissing extends \RuntimeException implements UriException
{
}

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\Exceptions;
use League\Uri\Contracts\UriException;
class IdnSupportMissing extends \RuntimeException implements UriException
{
}

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\IdnaInfo;
final class IdnaConversionFailed extends SyntaxError
{
/** @var IdnaInfo|null */
private $idnaInfo;
private function __construct(string $message, IdnaInfo $idnaInfo = null)
{
parent::__construct($message);
$this->idnaInfo = $idnaInfo;
}
public static function dueToIDNAError(string $domain, IdnaInfo $idnaInfo): self
{
return new self(
'The host `'.$domain.'` is invalid : '.implode(', ', $idnaInfo->errorList()).' .',
$idnaInfo
);
}
public static function dueToInvalidHost(string $domain): self
{
return new self('The host `'.$domain.'` is not a valid IDN host');
}
public function idnaInfo(): ?IdnaInfo
{
return $this->idnaInfo;
}
}

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\Exceptions;
use League\Uri\Contracts\UriException;
class SyntaxError extends \InvalidArgumentException implements UriException
{
}

View file

@ -0,0 +1,212 @@
<?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\IdnaConversionFailed;
use League\Uri\Exceptions\IdnSupportMissing;
use League\Uri\Exceptions\SyntaxError;
use function defined;
use function function_exists;
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 Idna
{
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';
/**
* IDNA options.
*/
public const IDNA_DEFAULT = 0;
public const IDNA_ALLOW_UNASSIGNED = 1;
public const IDNA_USE_STD3_RULES = 2;
public const IDNA_CHECK_BIDI = 4;
public const IDNA_CHECK_CONTEXTJ = 8;
public const IDNA_NONTRANSITIONAL_TO_ASCII = 0x10;
public const IDNA_NONTRANSITIONAL_TO_UNICODE = 0x20;
public const IDNA_CHECK_CONTEXTO = 0x40;
/**
* IDNA errors.
*/
public const ERROR_NONE = 0;
public const ERROR_EMPTY_LABEL = 1;
public const ERROR_LABEL_TOO_LONG = 2;
public const ERROR_DOMAIN_NAME_TOO_LONG = 4;
public const ERROR_LEADING_HYPHEN = 8;
public const ERROR_TRAILING_HYPHEN = 0x10;
public const ERROR_HYPHEN_3_4 = 0x20;
public const ERROR_LEADING_COMBINING_MARK = 0x40;
public const ERROR_DISALLOWED = 0x80;
public const ERROR_PUNYCODE = 0x100;
public const ERROR_LABEL_HAS_DOT = 0x200;
public const ERROR_INVALID_ACE_LABEL = 0x400;
public const ERROR_BIDI = 0x800;
public const ERROR_CONTEXTJ = 0x1000;
public const ERROR_CONTEXTO_PUNCTUATION = 0x2000;
public const ERROR_CONTEXTO_DIGITS = 0x4000;
/**
* IDNA default options.
*/
public const IDNA2008_ASCII = self::IDNA_NONTRANSITIONAL_TO_ASCII
| self::IDNA_CHECK_BIDI
| self::IDNA_USE_STD3_RULES
| self::IDNA_CHECK_CONTEXTJ;
public const IDNA2008_UNICODE = self::IDNA_NONTRANSITIONAL_TO_UNICODE
| self::IDNA_CHECK_BIDI
| self::IDNA_USE_STD3_RULES
| self::IDNA_CHECK_CONTEXTJ;
/**
* @codeCoverageIgnore
*/
private static function supportsIdna(): void
{
static $idnSupport;
if (null === $idnSupport) {
$idnSupport = function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46');
}
if (!$idnSupport) {
throw new IdnSupportMissing('IDN host can not be processed. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.');
}
}
/**
* Converts the input to its IDNA ASCII form.
*
* This method returns the string converted to IDN ASCII form
*
* @throws SyntaxError if the string can not be converted to ASCII using IDN UTS46 algorithm
*/
public static function toAscii(string $domain, int $options): IdnaInfo
{
$domain = rawurldecode($domain);
if (1 === preg_match(self::REGEXP_IDNA_PATTERN, $domain)) {
self::supportsIdna();
/* @param-out array{errors: int, isTransitionalDifferent: bool, result: string} $idnaInfo */
idn_to_ascii($domain, $options, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return IdnaInfo::fromIntl([
'result' => strtolower($domain),
'isTransitionalDifferent' => false,
'errors' => self::validateDomainAndLabelLength($domain),
]);
}
/* @var array{errors: int, isTransitionalDifferent: bool, result: string} $idnaInfo */
return IdnaInfo::fromIntl($idnaInfo);
}
$error = self::ERROR_NONE;
if (1 !== preg_match(self::REGEXP_REGISTERED_NAME, $domain)) {
$error |= self::ERROR_DISALLOWED;
}
return IdnaInfo::fromIntl([
'result' => strtolower($domain),
'isTransitionalDifferent' => false,
'errors' => self::validateDomainAndLabelLength($domain) | $error,
]);
}
/**
* Converts the input to its IDNA UNICODE form.
*
* This method returns the string converted to IDN UNICODE form
*
* @throws SyntaxError if the string can not be converted to UNICODE using IDN UTS46 algorithm
*/
public static function toUnicode(string $domain, int $options): IdnaInfo
{
$domain = rawurldecode($domain);
if (false === stripos($domain, 'xn--')) {
return IdnaInfo::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => self::ERROR_NONE]);
}
self::supportsIdna();
/* @param-out array{errors: int, isTransitionalDifferent: bool, result: string} $idnaInfo */
idn_to_utf8($domain, $options, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
throw IdnaConversionFailed::dueToInvalidHost($domain);
}
/* @var array{errors: int, isTransitionalDifferent: bool, result: string} $idnaInfo */
return IdnaInfo::fromIntl($idnaInfo);
}
/**
* 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 = self::ERROR_NONE;
$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 |= self::ERROR_DOMAIN_NAME_TOO_LONG;
}
foreach ($labels as $label) {
if (strlen($label) > self::MAX_LABEL_LENGTH) {
$error |= self::ERROR_LABEL_TOO_LONG;
break;
}
}
return $error;
}
}

View file

@ -0,0 +1,113 @@
<?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 function array_filter;
use const ARRAY_FILTER_USE_KEY;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class IdnaInfo
{
private const ERRORS = [
Idna::ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
Idna::ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
Idna::ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
Idna::ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
Idna::ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
Idna::ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
Idna::ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
Idna::ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
Idna::ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
Idna::ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
Idna::ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
Idna::ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
Idna::ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
Idna::ERROR_CONTEXTO_DIGITS => 'a label does not meet the IDNA CONTEXTO requirements for digits',
Idna::ERROR_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',
];
/** @var string */
private $result;
/** @var bool */
private $isTransitionalDifferent;
/** @var int */
private $errors;
/**
* @var array<int, string>
*/
private $errorList;
private function __construct(string $result, bool $isTransitionalDifferent, int $errors)
{
$this->result = $result;
$this->errors = $errors;
$this->isTransitionalDifferent = $isTransitionalDifferent;
$this->errorList = array_filter(
self::ERRORS,
function (int $error): bool {
return 0 !== ($error & $this->errors);
},
ARRAY_FILTER_USE_KEY
);
}
/**
* @param array{result:string, isTransitionalDifferent:bool, errors:int} $infos
*/
public static function fromIntl(array $infos): self
{
return new self($infos['result'], $infos['isTransitionalDifferent'], $infos['errors']);
}
/**
* @param array{result:string, isTransitionalDifferent:bool, errors:int} $properties
*/
public static function __set_state(array $properties): self
{
return self::fromIntl($properties);
}
public function result(): string
{
return $this->result;
}
public function isTransitionalDifferent(): bool
{
return $this->isTransitionalDifferent;
}
public function errors(): int
{
return $this->errors;
}
public function error(int $error): ?string
{
return $this->errorList[$error] ?? null;
}
/**
* @return array<int, string>
*/
public function errorList(): array
{
return $this->errorList;
}
}

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,110 @@
{
"name": "league/uri",
"type": "library",
"description" : "URI manipulation library",
"keywords": [
"url",
"uri",
"rfc3986",
"rfc3987",
"rfc6570",
"psr-7",
"parse_url",
"http",
"https",
"ws",
"ftp",
"data-uri",
"file-uri",
"middleware",
"parse_str",
"query-string",
"querystring",
"hostname",
"uri-template"
],
"license": "MIT",
"homepage": "http://uri.thephpleague.com",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://nyamsprod.com"
}
],
"support": {
"forum": "https://thephpleague.slack.com",
"docs": "https://uri.thephpleague.com",
"issues": "https://github.com/thephpleague/uri/issues"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php": ">=7.2",
"ext-json": "*",
"psr/http-message": "^1.0",
"league/uri-interfaces": "^2.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"phpunit/phpunit" : "^8.0 || ^9.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"psr/http-factory": "^1.0"
},
"autoload": {
"psr-4": {
"League\\Uri\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"LeagueTest\\Uri\\": "tests"
}
},
"conflict": {
"league/uri-schemes": "^1.0"
},
"scripts": {
"phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes --ansi",
"phpstan-src": "phpstan analyse -l max -c phpstan.src.neon src --ansi",
"phpstan-tests": "phpstan analyse -l max -c phpstan.tests.neon tests --ansi",
"phpstan": [
"@phpstan-src",
"@phpstan-tests"
],
"phpunit": "phpunit --coverage-text",
"test": [
"@phpcs",
"@phpstan",
"@phpunit"
]
},
"scripts-descriptions": {
"phpcs": "Runs coding style test suite",
"phpstan": "Runs complete codebase static analysis",
"phpstan-src": "Runs source code static analysis",
"phpstan-test": "Runs test suite static analysis",
"phpunit": "Runs unit and functional testing",
"test": "Runs full test suite"
},
"suggest": {
"league/uri-components" : "Needed to easily manipulate URI objects",
"ext-intl" : "Needed to improve host validation",
"ext-fileinfo": "Needed to create Data URI from a filepath",
"psr/http-factory": "Needed to use the URI factory"
},
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"config": {
"sort-packages": true
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use League\Uri\Contracts\UriException;
class TemplateCanNotBeExpanded extends \InvalidArgumentException implements UriException
{
public static function dueToUnableToProcessValueListWithPrefix(string $variableName): self
{
return new self('The ":" modifier can not be applied on "'.$variableName.'" since it is a list of values.');
}
public static function dueToNestedListOfValue(string $variableName): self
{
return new self('The "'.$variableName.'" can not be a nested list.');
}
}

View file

@ -0,0 +1,335 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use function is_object;
use function is_scalar;
use function method_exists;
use function sprintf;
final class Http implements Psr7UriInterface, \JsonSerializable
{
/**
* @var UriInterface
*/
private $uri;
/**
* New instance.
*/
private function __construct(UriInterface $uri)
{
$this->validate($uri);
$this->uri = $uri;
}
/**
* Validate the submitted uri against PSR-7 UriInterface.
*
* @throws SyntaxError if the given URI does not follow PSR-7 UriInterface rules
*/
private function validate(UriInterface $uri): void
{
$scheme = $uri->getScheme();
if (null === $scheme && '' === $uri->getHost()) {
throw new SyntaxError(sprintf('an URI without scheme can not contains a empty host string according to PSR-7: %s', (string) $uri));
}
$port = $uri->getPort();
if (null !== $port && ($port < 0 || $port > 65535)) {
throw new SyntaxError(sprintf('The URI port is outside the established TCP and UDP port ranges: %s', (string) $uri->getPort()));
}
}
/**
* Static method called by PHP's var export.
*/
public static function __set_state(array $components): self
{
return new self($components['uri']);
}
/**
* Create a new instance from a string.
*
* @param string|mixed $uri
*/
public static function createFromString($uri = ''): self
{
return new self(Uri::createFromString($uri));
}
/**
* Create a new instance from a hash of parse_url parts.
*
* @param array $components a hash representation of the URI similar
* to PHP parse_url function result
*/
public static function createFromComponents(array $components): self
{
return new self(Uri::createFromComponents($components));
}
/**
* Create a new instance from the environment.
*/
public static function createFromServer(array $server): self
{
return new self(Uri::createFromServer($server));
}
/**
* Create a new instance from a URI and a Base URI.
*
* The returned URI must be absolute.
*
* @param mixed $uri the input URI to create
* @param mixed $base_uri the base URI used for reference
*/
public static function createFromBaseUri($uri, $base_uri = null): self
{
return new self(Uri::createFromBaseUri($uri, $base_uri));
}
/**
* Create a new instance from a URI object.
*
* @param Psr7UriInterface|UriInterface $uri the input URI to create
*/
public static function createFromUri($uri): self
{
if ($uri instanceof UriInterface) {
return new self($uri);
}
return new self(Uri::createFromUri($uri));
}
/**
* {@inheritDoc}
*/
public function getScheme(): string
{
return (string) $this->uri->getScheme();
}
/**
* {@inheritDoc}
*/
public function getAuthority(): string
{
return (string) $this->uri->getAuthority();
}
/**
* {@inheritDoc}
*/
public function getUserInfo(): string
{
return (string) $this->uri->getUserInfo();
}
/**
* {@inheritDoc}
*/
public function getHost(): string
{
return (string) $this->uri->getHost();
}
/**
* {@inheritDoc}
*/
public function getPort(): ?int
{
return $this->uri->getPort();
}
/**
* {@inheritDoc}
*/
public function getPath(): string
{
return $this->uri->getPath();
}
/**
* {@inheritDoc}
*/
public function getQuery(): string
{
return (string) $this->uri->getQuery();
}
/**
* {@inheritDoc}
*/
public function getFragment(): string
{
return (string) $this->uri->getFragment();
}
/**
* {@inheritDoc}
*/
public function withScheme($scheme): self
{
$scheme = $this->filterInput($scheme);
if ('' === $scheme) {
$scheme = null;
}
$uri = $this->uri->withScheme($scheme);
if ($uri->getScheme() === $this->uri->getScheme()) {
return $this;
}
return new self($uri);
}
/**
* Safely stringify input when possible.
*
* @param mixed $str the value to evaluate as a string
*
* @throws SyntaxError if the submitted data can not be converted to string
*
* @return string|mixed
*/
private function filterInput($str)
{
if (is_scalar($str) || (is_object($str) && method_exists($str, '__toString'))) {
return (string) $str;
}
return $str;
}
/**
* {@inheritDoc}
*/
public function withUserInfo($user, $password = null): self
{
$user = $this->filterInput($user);
if ('' === $user) {
$user = null;
}
$uri = $this->uri->withUserInfo($user, $password);
if ($uri->getUserInfo() === $this->uri->getUserInfo()) {
return $this;
}
return new self($uri);
}
/**
* {@inheritDoc}
*/
public function withHost($host): self
{
$host = $this->filterInput($host);
if ('' === $host) {
$host = null;
}
$uri = $this->uri->withHost($host);
if ($uri->getHost() === $this->uri->getHost()) {
return $this;
}
return new self($uri);
}
/**
* {@inheritDoc}
*/
public function withPort($port): self
{
$uri = $this->uri->withPort($port);
if ($uri->getPort() === $this->uri->getPort()) {
return $this;
}
return new self($uri);
}
/**
* {@inheritDoc}
*/
public function withPath($path): self
{
$uri = $this->uri->withPath($path);
if ($uri->getPath() === $this->uri->getPath()) {
return $this;
}
return new self($uri);
}
/**
* {@inheritDoc}
*/
public function withQuery($query): self
{
$query = $this->filterInput($query);
if ('' === $query) {
$query = null;
}
$uri = $this->uri->withQuery($query);
if ($uri->getQuery() === $this->uri->getQuery()) {
return $this;
}
return new self($uri);
}
/**
* {@inheritDoc}
*/
public function withFragment($fragment): self
{
$fragment = $this->filterInput($fragment);
if ('' === $fragment) {
$fragment = null;
}
$uri = $this->uri->withFragment($fragment);
if ($uri->getFragment() === $this->uri->getFragment()) {
return $this;
}
return new self($uri);
}
/**
* {@inheritDoc}
*/
public function __toString(): string
{
return $this->uri->__toString();
}
/**
* {@inheritDoc}
*/
public function jsonSerialize(): string
{
return $this->uri->__toString();
}
}

View file

@ -0,0 +1,25 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
final class HttpFactory implements UriFactoryInterface
{
public function createUri(string $uri = ''): UriInterface
{
return Http::createFromString($uri);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,205 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use League\Uri\Contracts\UriInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use function explode;
use function implode;
use function preg_replace_callback;
use function rawurldecode;
use function sprintf;
final class UriInfo
{
private const REGEXP_ENCODED_CHARS = ',%(2[D|E]|3[0-9]|4[1-9|A-F]|5[0-9|A|F]|6[1-9|A-F]|7[0-9|E]),i';
private const WHATWG_SPECIAL_SCHEMES = ['ftp', 'http', 'https', 'ws', 'wss'];
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* @param Psr7UriInterface|UriInterface $uri
*/
private static function emptyComponentValue($uri): ?string
{
return $uri instanceof Psr7UriInterface ? '' : null;
}
/**
* Filter the URI object.
*
* To be valid an URI MUST implement at least one of the following interface:
* - League\Uri\UriInterface
* - Psr\Http\Message\UriInterface
*
* @param mixed $uri the URI to validate
*
* @throws \TypeError if the URI object does not implements the supported interfaces.
*
* @return Psr7UriInterface|UriInterface
*/
private static function filterUri($uri)
{
if ($uri instanceof Psr7UriInterface || $uri instanceof UriInterface) {
return $uri;
}
throw new \TypeError(sprintf('The uri must be a valid URI object received `%s`', is_object($uri) ? get_class($uri) : gettype($uri)));
}
/**
* Normalize an URI for comparison.
*
* @param Psr7UriInterface|UriInterface $uri
*
* @return Psr7UriInterface|UriInterface
*/
private static function normalize($uri)
{
$uri = self::filterUri($uri);
$null = self::emptyComponentValue($uri);
$path = $uri->getPath();
if ('/' === ($path[0] ?? '') || '' !== $uri->getScheme().$uri->getAuthority()) {
$path = UriResolver::resolve($uri, $uri->withPath('')->withQuery($null))->getPath();
}
$query = $uri->getQuery();
$fragment = $uri->getFragment();
$fragmentOrig = $fragment;
$pairs = null === $query ? [] : explode('&', $query);
sort($pairs, SORT_REGULAR);
$replace = static function (array $matches): string {
return rawurldecode($matches[0]);
};
$retval = preg_replace_callback(self::REGEXP_ENCODED_CHARS, $replace, [$path, implode('&', $pairs), $fragment]);
if (null !== $retval) {
[$path, $query, $fragment] = $retval + ['', $null, $null];
}
if ($null !== $uri->getAuthority() && '' === $path) {
$path = '/';
}
return $uri
->withHost(Uri::createFromComponents(['host' => $uri->getHost()])->getHost())
->withPath($path)
->withQuery([] === $pairs ? $null : $query)
->withFragment($null === $fragmentOrig ? $fragmentOrig : $fragment);
}
/**
* Tell whether the URI represents an absolute URI.
*
* @param Psr7UriInterface|UriInterface $uri
*/
public static function isAbsolute($uri): bool
{
return self::emptyComponentValue($uri) !== self::filterUri($uri)->getScheme();
}
/**
* Tell whether the URI represents a network path.
*
* @param Psr7UriInterface|UriInterface $uri
*/
public static function isNetworkPath($uri): bool
{
$uri = self::filterUri($uri);
$null = self::emptyComponentValue($uri);
return $null === $uri->getScheme() && $null !== $uri->getAuthority();
}
/**
* Tell whether the URI represents an absolute path.
*
* @param Psr7UriInterface|UriInterface $uri
*/
public static function isAbsolutePath($uri): bool
{
$uri = self::filterUri($uri);
$null = self::emptyComponentValue($uri);
return $null === $uri->getScheme()
&& $null === $uri->getAuthority()
&& '/' === ($uri->getPath()[0] ?? '');
}
/**
* Tell whether the URI represents a relative path.
*
* @param Psr7UriInterface|UriInterface $uri
*/
public static function isRelativePath($uri): bool
{
$uri = self::filterUri($uri);
$null = self::emptyComponentValue($uri);
return $null === $uri->getScheme()
&& $null === $uri->getAuthority()
&& '/' !== ($uri->getPath()[0] ?? '');
}
/**
* Tell whether both URI refers to the same document.
*
* @param Psr7UriInterface|UriInterface $uri
* @param Psr7UriInterface|UriInterface $base_uri
*/
public static function isSameDocument($uri, $base_uri): bool
{
$uri = self::normalize($uri);
$base_uri = self::normalize($base_uri);
return (string) $uri->withFragment($uri instanceof Psr7UriInterface ? '' : null)
=== (string) $base_uri->withFragment($base_uri instanceof Psr7UriInterface ? '' : null);
}
/**
* Returns the URI origin property as defined by WHATWG URL living standard.
*
* {@see https://url.spec.whatwg.org/#origin}
*
* For URI without a special scheme the method returns null
* For URI with the file scheme the method will return null (as this is left to the implementation decision)
* For URI with a special scheme the method returns the scheme followed by its authority (without the userinfo part)
*
* @param Psr7UriInterface|UriInterface $uri
*/
public static function getOrigin($uri): ?string
{
$scheme = self::filterUri($uri)->getScheme();
if ('blob' === $scheme) {
$uri = Uri::createFromString($uri->getPath());
$scheme = $uri->getScheme();
}
if (in_array($scheme, self::WHATWG_SPECIAL_SCHEMES, true)) {
$null = self::emptyComponentValue($uri);
return (string) $uri->withFragment($null)->withQuery($null)->withPath('')->withUserInfo($null, null);
}
return null;
}
}

View file

@ -0,0 +1,375 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use League\Uri\Contracts\UriInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use function array_pop;
use function array_reduce;
use function count;
use function end;
use function explode;
use function gettype;
use function implode;
use function in_array;
use function sprintf;
use function str_repeat;
use function strpos;
use function substr;
final class UriResolver
{
/**
* @var array<string,int>
*/
const DOT_SEGMENTS = ['.' => 1, '..' => 1];
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Resolve an URI against a base URI using RFC3986 rules.
*
* If the first argument is a UriInterface the method returns a UriInterface object
* If the first argument is a Psr7UriInterface the method returns a Psr7UriInterface object
*
* @param Psr7UriInterface|UriInterface $uri
* @param Psr7UriInterface|UriInterface $base_uri
*
* @return Psr7UriInterface|UriInterface
*/
public static function resolve($uri, $base_uri)
{
self::filterUri($uri);
self::filterUri($base_uri);
$null = $uri instanceof Psr7UriInterface ? '' : null;
if ($null !== $uri->getScheme()) {
return $uri
->withPath(self::removeDotSegments($uri->getPath()));
}
if ($null !== $uri->getAuthority()) {
return $uri
->withScheme($base_uri->getScheme())
->withPath(self::removeDotSegments($uri->getPath()));
}
$user = $null;
$pass = null;
$userInfo = $base_uri->getUserInfo();
if (null !== $userInfo) {
[$user, $pass] = explode(':', $userInfo, 2) + [1 => null];
}
[$uri_path, $uri_query] = self::resolvePathAndQuery($uri, $base_uri);
return $uri
->withPath(self::removeDotSegments($uri_path))
->withQuery($uri_query)
->withHost($base_uri->getHost())
->withPort($base_uri->getPort())
->withUserInfo((string) $user, $pass)
->withScheme($base_uri->getScheme())
;
}
/**
* Filter the URI object.
*
* @param mixed $uri an URI object
*
* @throws \TypeError if the URI object does not implements the supported interfaces.
*/
private static function filterUri($uri): void
{
if (!$uri instanceof UriInterface && !$uri instanceof Psr7UriInterface) {
throw new \TypeError(sprintf('The uri must be a valid URI object received `%s`', gettype($uri)));
}
}
/**
* Remove dot segments from the URI path.
*/
private static function removeDotSegments(string $path): string
{
if (false === strpos($path, '.')) {
return $path;
}
$old_segments = explode('/', $path);
$new_path = implode('/', array_reduce($old_segments, [UriResolver::class, 'reducer'], []));
if (isset(self::DOT_SEGMENTS[end($old_segments)])) {
$new_path .= '/';
}
// @codeCoverageIgnoreStart
// added because some PSR-7 implementations do not respect RFC3986
if (0 === strpos($path, '/') && 0 !== strpos($new_path, '/')) {
return '/'.$new_path;
}
// @codeCoverageIgnoreEnd
return $new_path;
}
/**
* Remove dot segments.
*
* @return array<int, string>
*/
private static function reducer(array $carry, string $segment): array
{
if ('..' === $segment) {
array_pop($carry);
return $carry;
}
if (!isset(self::DOT_SEGMENTS[$segment])) {
$carry[] = $segment;
}
return $carry;
}
/**
* Resolve an URI path and query component.
*
* @param Psr7UriInterface|UriInterface $uri
* @param Psr7UriInterface|UriInterface $base_uri
*
* @return array{0:string, 1:string|null}
*/
private static function resolvePathAndQuery($uri, $base_uri): array
{
$target_path = $uri->getPath();
$target_query = $uri->getQuery();
$null = $uri instanceof Psr7UriInterface ? '' : null;
$baseNull = $base_uri instanceof Psr7UriInterface ? '' : null;
if (0 === strpos($target_path, '/')) {
return [$target_path, $target_query];
}
if ('' === $target_path) {
if ($null === $target_query) {
$target_query = $base_uri->getQuery();
}
$target_path = $base_uri->getPath();
//@codeCoverageIgnoreStart
//because some PSR-7 Uri implementations allow this RFC3986 forbidden construction
if ($baseNull !== $base_uri->getAuthority() && 0 !== strpos($target_path, '/')) {
$target_path = '/'.$target_path;
}
//@codeCoverageIgnoreEnd
return [$target_path, $target_query];
}
$base_path = $base_uri->getPath();
if ($baseNull !== $base_uri->getAuthority() && '' === $base_path) {
$target_path = '/'.$target_path;
}
if ('' !== $base_path) {
$segments = explode('/', $base_path);
array_pop($segments);
if ([] !== $segments) {
$target_path = implode('/', $segments).'/'.$target_path;
}
}
return [$target_path, $target_query];
}
/**
* Relativize an URI according to a base URI.
*
* This method MUST retain the state of the submitted URI instance, and return
* an URI instance of the same type that contains the applied modifications.
*
* This method MUST be transparent when dealing with error and exceptions.
* It MUST not alter of silence them apart from validating its own parameters.
*
* @param Psr7UriInterface|UriInterface $uri
* @param Psr7UriInterface|UriInterface $base_uri
*
* @return Psr7UriInterface|UriInterface
*/
public static function relativize($uri, $base_uri)
{
self::filterUri($uri);
self::filterUri($base_uri);
$uri = self::formatHost($uri);
$base_uri = self::formatHost($base_uri);
if (!self::isRelativizable($uri, $base_uri)) {
return $uri;
}
$null = $uri instanceof Psr7UriInterface ? '' : null;
$uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null);
$target_path = $uri->getPath();
if ($target_path !== $base_uri->getPath()) {
return $uri->withPath(self::relativizePath($target_path, $base_uri->getPath()));
}
if (self::componentEquals('getQuery', $uri, $base_uri)) {
return $uri->withPath('')->withQuery($null);
}
if ($null === $uri->getQuery()) {
return $uri->withPath(self::formatPathWithEmptyBaseQuery($target_path));
}
return $uri->withPath('');
}
/**
* Tells whether the component value from both URI object equals.
*
* @param Psr7UriInterface|UriInterface $uri
* @param Psr7UriInterface|UriInterface $base_uri
*/
private static function componentEquals(string $method, $uri, $base_uri): bool
{
return self::getComponent($method, $uri) === self::getComponent($method, $base_uri);
}
/**
* Returns the component value from the submitted URI object.
*
* @param Psr7UriInterface|UriInterface $uri
*/
private static function getComponent(string $method, $uri): ?string
{
$component = $uri->$method();
if ($uri instanceof Psr7UriInterface && '' === $component) {
return null;
}
return $component;
}
/**
* Filter the URI object.
*
* @param null|mixed $uri
*
* @throws \TypeError if the URI object does not implements the supported interfaces.
*
* @return Psr7UriInterface|UriInterface
*/
private static function formatHost($uri)
{
if (!$uri instanceof Psr7UriInterface) {
return $uri;
}
$host = $uri->getHost();
if ('' === $host) {
return $uri;
}
return $uri->withHost((string) Uri::createFromComponents(['host' => $host])->getHost());
}
/**
* Tell whether the submitted URI object can be relativize.
*
* @param Psr7UriInterface|UriInterface $uri
* @param Psr7UriInterface|UriInterface $base_uri
*/
private static function isRelativizable($uri, $base_uri): bool
{
return !UriInfo::isRelativePath($uri)
&& self::componentEquals('getScheme', $uri, $base_uri)
&& self::componentEquals('getAuthority', $uri, $base_uri);
}
/**
* Relative the URI for a authority-less target URI.
*/
private static function relativizePath(string $path, string $basepath): string
{
$base_segments = self::getSegments($basepath);
$target_segments = self::getSegments($path);
$target_basename = array_pop($target_segments);
array_pop($base_segments);
foreach ($base_segments as $offset => $segment) {
if (!isset($target_segments[$offset]) || $segment !== $target_segments[$offset]) {
break;
}
unset($base_segments[$offset], $target_segments[$offset]);
}
$target_segments[] = $target_basename;
return self::formatPath(
str_repeat('../', count($base_segments)).implode('/', $target_segments),
$basepath
);
}
/**
* returns the path segments.
*
* @return string[]
*/
private static function getSegments(string $path): array
{
if ('' !== $path && '/' === $path[0]) {
$path = substr($path, 1);
}
return explode('/', $path);
}
/**
* Formatting the path to keep a valid URI.
*/
private static function formatPath(string $path, string $basepath): string
{
if ('' === $path) {
return in_array($basepath, ['', '/'], true) ? $basepath : './';
}
if (false === ($colon_pos = strpos($path, ':'))) {
return $path;
}
$slash_pos = strpos($path, '/');
if (false === $slash_pos || $colon_pos < $slash_pos) {
return "./$path";
}
return $path;
}
/**
* Formatting the path to keep a resolvable URI.
*/
private static function formatPathWithEmptyBaseQuery(string $path): string
{
$target_segments = self::getSegments($path);
/** @var string $basename */
$basename = end($target_segments);
return '' === $basename ? './' : $basename;
}
}

View file

@ -0,0 +1,567 @@
<?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\IdnSupportMissing;
use League\Uri\Exceptions\SyntaxError;
use function array_merge;
use function defined;
use function explode;
use function filter_var;
use function function_exists;
use function gettype;
use function idn_to_ascii;
use function implode;
use function inet_pton;
use function is_object;
use function is_scalar;
use function method_exists;
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;
use const IDNA_ERROR_BIDI;
use const IDNA_ERROR_CONTEXTJ;
use const IDNA_ERROR_DISALLOWED;
use const IDNA_ERROR_DOMAIN_NAME_TOO_LONG;
use const IDNA_ERROR_EMPTY_LABEL;
use const IDNA_ERROR_HYPHEN_3_4;
use const IDNA_ERROR_INVALID_ACE_LABEL;
use const IDNA_ERROR_LABEL_HAS_DOT;
use const IDNA_ERROR_LABEL_TOO_LONG;
use const IDNA_ERROR_LEADING_COMBINING_MARK;
use const IDNA_ERROR_LEADING_HYPHEN;
use const IDNA_ERROR_PUNYCODE;
use const IDNA_ERROR_TRAILING_HYPHEN;
use const INTL_IDNA_VARIANT_UTS46;
/**
* 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
*/
final class UriString
{
/**
* Default URI component values.
*/
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.
*/
private const URI_SCHORTCUTS = [
'' => [],
'#' => ['fragment' => ''],
'?' => ['query' => ''],
'?#' => ['query' => '', 'fragment' => ''],
'/' => ['path' => '/'],
'//' => ['host' => ''],
];
/**
* Range of invalid characters in URI string.
*/
private const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
/**
* RFC3986 regular expression URI splitter.
*
* @link https://tools.ietf.org/html/rfc3986#appendix-B
*/
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 expresssion.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.1
*/
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
*/
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
*/
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
*/
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
*/
private const REGEXP_INVALID_PATH = ',^(([^/]*):)(.*)?/,';
/**
* Host and Port splitter regular expression.
*/
private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,';
/**
* IDN Host detector regular expression.
*/
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.
*/
private const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80";
/**
* Generate an URI string representation from its parsed representation
* returned by League\Uri\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 array{
* scheme:?string,
* user:?string,
* pass:?string,
* host:?string,
* port:?int,
* path:string,
* query:?string,
* fragment:?string
* } $components
*/
public static function build(array $components): string
{
$result = $components['path'] ?? '';
if (isset($components['query'])) {
$result .= '?'.$components['query'];
}
if (isset($components['fragment'])) {
$result .= '#'.$components['fragment'];
}
$scheme = null;
if (isset($components['scheme'])) {
$scheme = $components['scheme'].':';
}
if (!isset($components['host'])) {
return $scheme.$result;
}
$scheme .= '//';
$authority = $components['host'];
if (isset($components['port'])) {
$authority .= ':'.$components['port'];
}
if (!isset($components['user'])) {
return $scheme.$authority.$result;
}
$authority = '@'.$authority;
if (!isset($components['pass'])) {
return $scheme.$components['user'].$authority.$result;
}
return $scheme.$components['user'].':'.$components['pass'].$authority.$result;
}
/**
* Parse an 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 = (new Parser())->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
*
* @param mixed $uri any scalar or stringable object
*
* @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 array{
* scheme:?string,
* user:?string,
* pass:?string,
* host:?string,
* port:?int,
* path:string,
* query:?string,
* fragment:?string
* }
*/
public static function parse($uri): array
{
if (is_object($uri) && method_exists($uri, '__toString')) {
$uri = (string) $uri;
}
if (!is_scalar($uri)) {
throw new \TypeError(sprintf('The uri must be a scalar or a stringable object `%s` given', gettype($uri)));
}
$uri = (string) $uri;
if (isset(self::URI_SCHORTCUTS[$uri])) {
/** @var array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} $components */
$components = array_merge(self::URI_COMPONENTS, self::URI_SCHORTCUTS[$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'] || 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 array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} $components */
$components = array_merge(
self::URI_COMPONENTS,
'' === $parts['authority'] ? [] : self::parseAuthority($parts['acontent']),
[
'path' => $parts['path'],
'scheme' => '' === $parts['scheme'] ? null : $parts['scontent'],
'query' => '' === $parts['query'] ? null : $parts['qcontent'],
'fragment' => '' === $parts['fragment'] ? null : $parts['fcontent'],
]
);
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 array{user:?string, pass:?string, host:?string, port:?int}
*/
private static function parseAuthority(string $authority): array
{
$components = ['user' => null, 'pass' => null, 'host' => '', 'port' => null];
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
{
if ('' === $port) {
return null;
}
if (1 === preg_match('/^\d*$/', $port)) {
return (int) $port;
}
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;
}
if ('[' !== $host[0] || ']' !== substr($host, -1)) {
return self::filterRegisteredName($host);
}
if (!self::isIpHost(substr($host, 1, -1))) {
throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
}
return $host;
}
/**
* Returns whether the host is an IPv4 or a registered named.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
* @throws IdnSupportMissing if IDN support or ICU requirement are not available or met.
*/
private static function filterRegisteredName(string $host): string
{
// @codeCoverageIgnoreStart
// added because it is not possible in travis to disabled the ext/intl extension
// see travis issue https://github.com/travis-ci/travis-ci/issues/4701
static $idn_support = null;
$idn_support = $idn_support ?? function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46');
// @codeCoverageIgnoreEnd
$formatted_host = rawurldecode($host);
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formatted_host)) {
if (false === strpos($formatted_host, 'xn--')) {
return $host;
}
// @codeCoverageIgnoreStart
if (!$idn_support) {
throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
}
// @codeCoverageIgnoreEnd
$unicode = idn_to_utf8($host, 0, INTL_IDNA_VARIANT_UTS46, $arr);
if (0 !== $arr['errors']) {
throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, self::getIDNAErrors($arr['errors'])));
}
// @codeCoverageIgnoreStart
if (false === $unicode) {
throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
}
// @codeCoverageIgnoreEnd
return $host;
}
//to test IDN host non-ascii characters must be present in the host
if (1 !== preg_match(self::REGEXP_IDN_PATTERN, $formatted_host)) {
throw new SyntaxError(sprintf('Host `%s` is invalid : the host is not a valid registered name', $host));
}
// @codeCoverageIgnoreStart
if (!$idn_support) {
throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
}
// @codeCoverageIgnoreEnd
$retval = idn_to_ascii($formatted_host, 0, INTL_IDNA_VARIANT_UTS46, $arr);
if ([] === $arr) {
throw new SyntaxError(sprintf('Host `%s` is not a valid IDN host', $host));
}
if (0 !== $arr['errors']) {
throw new SyntaxError(sprintf('Host `%s` is not a valid IDN host : %s', $host, self::getIDNAErrors($arr['errors'])));
}
// @codeCoverageIgnoreStart
if (false === $retval) {
throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
}
// @codeCoverageIgnoreEnd
if (false !== strpos($retval, '%')) {
throw new SyntaxError(sprintf('Host `%s` is invalid : the host is not a valid registered name', $host));
}
return $host;
}
/**
* Retrieves and format IDNA conversion error message.
*
* @link http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html
*/
private static function getIDNAErrors(int $error_byte): string
{
/**
* IDNA errors.
*/
static $idn_errors = [
IDNA_ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
IDNA_ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
IDNA_ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
IDNA_ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
IDNA_ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
IDNA_ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
IDNA_ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
IDNA_ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
IDNA_ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
IDNA_ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
IDNA_ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
IDNA_ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
];
$res = [];
foreach ($idn_errors as $error => $reason) {
if ($error === ($error_byte & $error)) {
$res[] = $reason;
}
}
return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.';
}
/**
* Validates a IPv6/IPvfuture 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 $ip_host): bool
{
if (false !== filter_var($ip_host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return true;
}
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ip_host, $matches)) {
return !in_array($matches['version'], ['4', '6'], true);
}
$pos = strpos($ip_host, '%');
if (false === $pos || 1 === preg_match(
self::REGEXP_INVALID_HOST_CHARS,
rawurldecode(substr($ip_host, $pos))
)) {
return false;
}
$ip_host = substr($ip_host, 0, $pos);
return false !== filter_var($ip_host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& 0 === strpos((string) inet_pton($ip_host), self::ZONE_ID_ADDRESS_BLOCK);
}
}

View file

@ -0,0 +1,140 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Exceptions\TemplateCanNotBeExpanded;
use League\Uri\UriTemplate\Template;
use League\Uri\UriTemplate\VariableBag;
/**
* Defines the URI Template syntax and the process for expanding a URI Template into a URI reference.
*
* @link https://tools.ietf.org/html/rfc6570
* @package League\Uri
* @author Ignace Nyamagana Butera <nyamsprod@gmail.com>
* @since 6.1.0
*
* Based on GuzzleHttp\UriTemplate class in Guzzle v6.5.
* @link https://github.com/guzzle/guzzle/blob/6.5/src/UriTemplate.php
*/
final class UriTemplate
{
/**
* @var Template
*/
private $template;
/**
* @var VariableBag
*/
private $defaultVariables;
/**
* @param object|string $template a string or an object with the __toString method
*
* @throws \TypeError if the template is not a string or an object with the __toString method
* @throws SyntaxError if the template syntax is invalid
* @throws TemplateCanNotBeExpanded if the template variables are invalid
*/
public function __construct($template, array $defaultVariables = [])
{
$this->template = Template::createFromString($template);
$this->defaultVariables = $this->filterVariables($defaultVariables);
}
public static function __set_state(array $properties): self
{
return new self($properties['template']->toString(), $properties['defaultVariables']->all());
}
/**
* Filters out variables for the given template.
*
* @param array<string,string|array<string>> $variables
*/
private function filterVariables(array $variables): VariableBag
{
$output = new VariableBag();
foreach ($this->template->variableNames() as $name) {
if (isset($variables[$name])) {
$output->assign($name, $variables[$name]);
}
}
return $output;
}
/**
* The template string.
*/
public function getTemplate(): string
{
return $this->template->toString();
}
/**
* Returns the names of the variables in the template, in order.
*
* @return string[]
*/
public function getVariableNames(): array
{
return $this->template->variableNames();
}
/**
* Returns the default values used to expand the template.
*
* The returned list only contains variables whose name is part of the current template.
*
* @return array<string,string|array>
*/
public function getDefaultVariables(): array
{
return $this->defaultVariables->all();
}
/**
* Returns a new instance with the updated default variables.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified default variables.
*
* If present, variables whose name is not part of the current template
* possible variable names are removed.
*/
public function withDefaultVariables(array $defaultDefaultVariables): self
{
$clone = clone $this;
$clone->defaultVariables = $this->filterVariables($defaultDefaultVariables);
return $clone;
}
/**
* @throws TemplateCanNotBeExpanded if the variable contains nested array values
* @throws UriException if the resulting expansion can not be converted to a UriInterface instance
*/
public function expand(array $variables = []): UriInterface
{
$uriString = $this->template->expand(
$this->filterVariables($variables)->replace($this->defaultVariables)
);
return Uri::createFromString($uriString);
}
}

View file

@ -0,0 +1,353 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\UriTemplate;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Exceptions\TemplateCanNotBeExpanded;
use function array_filter;
use function array_keys;
use function array_map;
use function array_unique;
use function explode;
use function implode;
use function preg_match;
use function rawurlencode;
use function str_replace;
use function strpos;
use function substr;
final class Expression
{
/**
* Expression regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc6570#section-2.2
*/
private const REGEXP_EXPRESSION = '/^\{
(?:
(?<operator>[\.\/;\?&\=,\!@\|\+#])?
(?<variables>[^\}]*)
)
\}$/x';
/**
* Reserved Operator characters.
*
* @link https://tools.ietf.org/html/rfc6570#section-2.2
*/
private const RESERVED_OPERATOR = '=,!@|';
/**
* Processing behavior according to the expression type operator.
*
* @link https://tools.ietf.org/html/rfc6570#appendix-A
*/
private const OPERATOR_HASH_LOOKUP = [
'' => ['prefix' => '', 'joiner' => ',', 'query' => false],
'+' => ['prefix' => '', 'joiner' => ',', 'query' => false],
'#' => ['prefix' => '#', 'joiner' => ',', 'query' => false],
'.' => ['prefix' => '.', 'joiner' => '.', 'query' => false],
'/' => ['prefix' => '/', 'joiner' => '/', 'query' => false],
';' => ['prefix' => ';', 'joiner' => ';', 'query' => true],
'?' => ['prefix' => '?', 'joiner' => '&', 'query' => true],
'&' => ['prefix' => '&', 'joiner' => '&', 'query' => true],
];
/**
* @var string
*/
private $operator;
/**
* @var string
*/
private $joiner;
/**
* @var array<VarSpecifier>
*/
private $varSpecifiers;
/**
* @var array<string>
*/
private $variableNames;
/**
* @var string
*/
private $expressionString;
private function __construct(string $operator, VarSpecifier ...$varSpecifiers)
{
$this->operator = $operator;
$this->varSpecifiers = $varSpecifiers;
$this->joiner = self::OPERATOR_HASH_LOOKUP[$operator]['joiner'];
$this->variableNames = $this->setVariableNames();
$this->expressionString = $this->setExpressionString();
}
/**
* @return array<string>
*/
private function setVariableNames(): array
{
$mapper = static function (VarSpecifier $varSpecifier): string {
return $varSpecifier->name();
};
return array_unique(array_map($mapper, $this->varSpecifiers));
}
private function setExpressionString(): string
{
$mapper = static function (VarSpecifier $variable): string {
return $variable->toString();
};
$varSpecifierString = implode(',', array_map($mapper, $this->varSpecifiers));
return '{'.$this->operator.$varSpecifierString.'}';
}
/**
* {@inheritDoc}
*/
public static function __set_state(array $properties): self
{
return new self($properties['operator'], ...$properties['varSpecifiers']);
}
/**
* @throws SyntaxError if the expression is invalid
* @throws SyntaxError if the operator used in the expression is invalid
* @throws SyntaxError if the variable specifiers is invalid
*/
public static function createFromString(string $expression): self
{
if (1 !== preg_match(self::REGEXP_EXPRESSION, $expression, $parts)) {
throw new SyntaxError('The expression "'.$expression.'" is invalid.');
}
/** @var array{operator:string, variables:string} $parts */
$parts = $parts + ['operator' => ''];
if ('' !== $parts['operator'] && false !== strpos(self::RESERVED_OPERATOR, $parts['operator'])) {
throw new SyntaxError('The operator used in the expression "'.$expression.'" is reserved.');
}
$mapper = static function (string $varSpec): VarSpecifier {
return VarSpecifier::createFromString($varSpec);
};
return new Expression($parts['operator'], ...array_map($mapper, explode(',', $parts['variables'])));
}
/**
* Returns the expression string representation.
*
*/
public function toString(): string
{
return $this->expressionString;
}
/**
* @return array<string>
*/
public function variableNames(): array
{
return $this->variableNames;
}
public function expand(VariableBag $variables): string
{
$parts = [];
foreach ($this->varSpecifiers as $varSpecifier) {
$parts[] = $this->replace($varSpecifier, $variables);
}
$nullFilter = static function ($value): bool {
return '' !== $value;
};
$expanded = implode($this->joiner, array_filter($parts, $nullFilter));
if ('' === $expanded) {
return $expanded;
}
$prefix = self::OPERATOR_HASH_LOOKUP[$this->operator]['prefix'];
if ('' === $prefix) {
return $expanded;
}
return $prefix.$expanded;
}
/**
* Replaces an expression with the given variables.
*
* @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
* @throws TemplateCanNotBeExpanded if the variables contains nested array values
*/
private function replace(VarSpecifier $varSpec, VariableBag $variables): string
{
$value = $variables->fetch($varSpec->name());
if (null === $value) {
return '';
}
$useQuery = self::OPERATOR_HASH_LOOKUP[$this->operator]['query'];
[$expanded, $actualQuery] = $this->inject($value, $varSpec, $useQuery);
if (!$actualQuery) {
return $expanded;
}
if ('&' !== $this->joiner && '' === $expanded) {
return $varSpec->name();
}
return $varSpec->name().'='.$expanded;
}
/**
* @param string|array<string> $value
*
* @return array{0:string, 1:bool}
*/
private function inject($value, VarSpecifier $varSpec, bool $useQuery): array
{
if (is_string($value)) {
return $this->replaceString($value, $varSpec, $useQuery);
}
return $this->replaceList($value, $varSpec, $useQuery);
}
/**
* Expands an expression using a string value.
*
* @return array{0:string, 1:bool}
*/
private function replaceString(string $value, VarSpecifier $varSpec, bool $useQuery): array
{
if (':' === $varSpec->modifier()) {
$value = substr($value, 0, $varSpec->position());
}
$expanded = rawurlencode($value);
if ('+' === $this->operator || '#' === $this->operator) {
return [$this->decodeReserved($expanded), $useQuery];
}
return [$expanded, $useQuery];
}
/**
* Expands an expression using a list of values.
*
* @param array<string> $value
*
* @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
*
* @return array{0:string, 1:bool}
*/
private function replaceList(array $value, VarSpecifier $varSpec, bool $useQuery): array
{
if ([] === $value) {
return ['', false];
}
if (':' === $varSpec->modifier()) {
throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name());
}
$pairs = [];
$isAssoc = $this->isAssoc($value);
foreach ($value as $key => $var) {
if ($isAssoc) {
$key = rawurlencode((string) $key);
}
$var = rawurlencode($var);
if ('+' === $this->operator || '#' === $this->operator) {
$var = $this->decodeReserved($var);
}
if ('*' === $varSpec->modifier()) {
if ($isAssoc) {
$var = $key.'='.$var;
} elseif ($key > 0 && $useQuery) {
$var = $varSpec->name().'='.$var;
}
}
$pairs[$key] = $var;
}
if ('*' === $varSpec->modifier()) {
if ($isAssoc) {
// Don't prepend the value name when using the explode
// modifier with an associative array.
$useQuery = false;
}
return [implode($this->joiner, $pairs), $useQuery];
}
if ($isAssoc) {
// When an associative array is encountered and the
// explode modifier is not set, then the result must be
// a comma separated list of keys followed by their
// respective values.
foreach ($pairs as $offset => &$data) {
$data = $offset.','.$data;
}
unset($data);
}
return [implode(',', $pairs), $useQuery];
}
/**
* Determines if an array is associative.
*
* This makes the assumption that input arrays are sequences or hashes.
* This assumption is a trade-off for accuracy in favor of speed, but it
* should work in almost every case where input is supplied for a URI
* template.
*/
private function isAssoc(array $array): bool
{
return [] !== $array && 0 !== array_keys($array)[0];
}
/**
* Removes percent encoding on reserved characters (used with + and # modifiers).
*/
private function decodeReserved(string $str): string
{
static $delimiters = [
':', '/', '?', '#', '[', ']', '@', '!', '$',
'&', '\'', '(', ')', '*', '+', ',', ';', '=',
];
static $delimitersEncoded = [
'%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%21', '%24',
'%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', '%3B', '%3D',
];
return str_replace($delimitersEncoded, $delimiters, $str);
}
}

View file

@ -0,0 +1,134 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\UriTemplate;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Exceptions\TemplateCanNotBeExpanded;
use function array_merge;
use function array_unique;
use function gettype;
use function is_object;
use function is_string;
use function method_exists;
use function preg_match_all;
use function preg_replace;
use function sprintf;
use function strpos;
use const PREG_SET_ORDER;
final class Template
{
/**
* Expression regular expression pattern.
*/
private const REGEXP_EXPRESSION_DETECTOR = '/\{[^\}]*\}/x';
/**
* @var string
*/
private $template;
/**
* @var array<string, Expression>
*/
private $expressions = [];
/**
* @var array<string>
*/
private $variableNames;
private function __construct(string $template, Expression ...$expressions)
{
$this->template = $template;
$variableNames = [];
foreach ($expressions as $expression) {
$this->expressions[$expression->toString()] = $expression;
$variableNames[] = $expression->variableNames();
}
$this->variableNames = array_unique(array_merge([], ...$variableNames));
}
/**
* {@inheritDoc}
*/
public static function __set_state(array $properties): self
{
return new self($properties['template'], ...array_values($properties['expressions']));
}
/**
* @param object|string $template a string or an object with the __toString method
*
* @throws \TypeError if the template is not a string or an object with the __toString method
* @throws SyntaxError if the template contains invalid expressions
* @throws SyntaxError if the template contains invalid variable specification
*/
public static function createFromString($template): self
{
if (is_object($template) && method_exists($template, '__toString')) {
$template = (string) $template;
}
if (!is_string($template)) {
throw new \TypeError(sprintf('The template must be a string or a stringable object %s given.', gettype($template)));
}
/** @var string $remainder */
$remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template);
if (false !== strpos($remainder, '{') || false !== strpos($remainder, '}')) {
throw new SyntaxError('The template "'.$template.'" contains invalid expressions.');
}
$names = [];
preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $findings, PREG_SET_ORDER);
$arguments = [];
foreach ($findings as $finding) {
if (!isset($names[$finding[0]])) {
$arguments[] = Expression::createFromString($finding[0]);
$names[$finding[0]] = 1;
}
}
return new self($template, ...$arguments);
}
public function toString(): string
{
return $this->template;
}
/**
* @return array<string>
*/
public function variableNames(): array
{
return $this->variableNames;
}
/**
* @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
* @throws TemplateCanNotBeExpanded if the variables contains nested array values
*/
public function expand(VariableBag $variables): string
{
$uriString = $this->template;
/** @var Expression $expression */
foreach ($this->expressions as $pattern => $expression) {
$uriString = str_replace($pattern, $expression->expand($variables), $uriString);
}
return $uriString;
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\UriTemplate;
use League\Uri\Exceptions\SyntaxError;
use function preg_match;
final class VarSpecifier
{
/**
* Variables specification regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc6570#section-2.3
*/
private const REGEXP_VARSPEC = '/^
(?<name>(?:[A-z0-9_\.]|%[0-9a-fA-F]{2})+)
(?<modifier>\:(?<position>\d+)|\*)?
$/x';
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $modifier;
/**
* @var int
*/
private $position;
private function __construct(string $name, string $modifier, int $position)
{
$this->name = $name;
$this->modifier = $modifier;
$this->position = $position;
}
/**
* {@inheritDoc}
*/
public static function __set_state(array $properties): self
{
return new self($properties['name'], $properties['modifier'], $properties['position']);
}
public static function createFromString(string $specification): self
{
if (1 !== preg_match(self::REGEXP_VARSPEC, $specification, $parsed)) {
throw new SyntaxError('The variable specification "'.$specification.'" is invalid.');
}
$parsed += ['modifier' => '', 'position' => ''];
if ('' !== $parsed['position']) {
$parsed['position'] = (int) $parsed['position'];
$parsed['modifier'] = ':';
}
if ('' === $parsed['position']) {
$parsed['position'] = 0;
}
if (10000 <= $parsed['position']) {
throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be lower than 10000.');
}
return new self($parsed['name'], $parsed['modifier'], $parsed['position']);
}
public function toString(): string
{
if (0 < $this->position) {
return $this->name.$this->modifier.$this->position;
}
return $this->name.$this->modifier;
}
public function name(): string
{
return $this->name;
}
public function modifier(): string
{
return $this->modifier;
}
public function position(): int
{
return $this->position;
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\UriTemplate;
use League\Uri\Exceptions\TemplateCanNotBeExpanded;
use function gettype;
use function is_array;
use function is_bool;
use function is_object;
use function is_scalar;
use function method_exists;
use function sprintf;
final class VariableBag
{
/**
* @var array<string,string|array<string>>
*/
private $variables = [];
/**
* @param iterable<string,mixed> $variables
*/
public function __construct(iterable $variables = [])
{
foreach ($variables as $name => $value) {
$this->assign($name, $value);
}
}
public static function __set_state(array $properties): self
{
return new self($properties['variables']);
}
/**
* @return array<string,string|array<string>>
*/
public function all(): array
{
return $this->variables;
}
/**
* Fetches the variable value if none found returns null.
*
* @return null|string|array<string>
*/
public function fetch(string $name)
{
return $this->variables[$name] ?? null;
}
/**
* @param string|array<string> $value
*/
public function assign(string $name, $value): void
{
$this->variables[$name] = $this->normalizeValue($value, $name, true);
}
/**
* @param mixed $value the value to be expanded
*
* @throws TemplateCanNotBeExpanded if the value contains nested list
*
* @return string|array<string>
*/
private function normalizeValue($value, string $name, bool $isNestedListAllowed)
{
if (is_bool($value)) {
return true === $value ? '1' : '0';
}
if (null === $value || is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
return (string) $value;
}
if (!is_array($value)) {
throw new \TypeError(sprintf('The variable '.$name.' must be NULL, a scalar or a stringable object `%s` given', gettype($value)));
}
if (!$isNestedListAllowed) {
throw TemplateCanNotBeExpanded::dueToNestedListOfValue($name);
}
foreach ($value as &$var) {
$var = self::normalizeValue($var, $name, false);
}
unset($var);
return $value;
}
/**
* Replaces elements from passed variables into the current instance.
*/
public function replace(VariableBag $variables): self
{
$instance = clone $this;
$instance->variables += $variables->variables;
return $instance;
}
}