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