Update website

This commit is contained in:
Guilhem Lavaux 2024-11-23 20:45:29 +01:00
parent 41ce1aa076
commit ea0eb1c6e0
4222 changed files with 721797 additions and 14 deletions

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