359 lines
10 KiB
PHP
359 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* League.Uri (https://uri.thephpleague.com)
|
|
*
|
|
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace League\Uri;
|
|
|
|
use BackedEnum;
|
|
use League\Uri\Contracts\Conditionable;
|
|
use League\Uri\Contracts\FragmentDirective;
|
|
use League\Uri\Contracts\Transformable;
|
|
use League\Uri\Contracts\UriComponentInterface;
|
|
use League\Uri\Exceptions\SyntaxError;
|
|
use SensitiveParameter;
|
|
use Stringable;
|
|
use Throwable;
|
|
use TypeError;
|
|
use Uri\Rfc3986\Uri as Rfc3986Uri;
|
|
use Uri\WhatWg\Url as WhatWgUrl;
|
|
|
|
use function is_bool;
|
|
use function str_replace;
|
|
use function strpos;
|
|
|
|
final class Builder implements Conditionable, Transformable
|
|
{
|
|
private ?string $scheme = null;
|
|
private ?string $username = null;
|
|
private ?string $password = null;
|
|
private ?string $host = null;
|
|
private ?int $port = null;
|
|
private ?string $path = null;
|
|
private ?string $query = null;
|
|
private ?string $fragment = null;
|
|
|
|
public function __construct(
|
|
BackedEnum|Stringable|string|null $scheme = null,
|
|
BackedEnum|Stringable|string|null $username = null,
|
|
#[SensitiveParameter] BackedEnum|Stringable|string|null $password = null,
|
|
BackedEnum|Stringable|string|null $host = null,
|
|
BackedEnum|int|null $port = null,
|
|
BackedEnum|Stringable|string|null $path = null,
|
|
BackedEnum|Stringable|string|null $query = null,
|
|
BackedEnum|Stringable|string|null $fragment = null,
|
|
) {
|
|
$this
|
|
->scheme($scheme)
|
|
->userInfo($username, $password)
|
|
->host($host)
|
|
->port($port)
|
|
->path($path)
|
|
->query($query)
|
|
->fragment($fragment);
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function scheme(BackedEnum|Stringable|string|null $scheme): self
|
|
{
|
|
$scheme = $this->filterString($scheme);
|
|
if ($scheme !== $this->scheme) {
|
|
UriString::isValidScheme($scheme) || throw new SyntaxError('The scheme `'.$scheme.'` is invalid.');
|
|
|
|
$this->scheme = $scheme;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function userInfo(
|
|
BackedEnum|Stringable|string|null $user,
|
|
#[SensitiveParameter] BackedEnum|Stringable|string|null $password = null
|
|
): static {
|
|
$username = Encoder::encodeUser($this->filterString($user));
|
|
$password = Encoder::encodePassword($this->filterString($password));
|
|
if ($username !== $this->username || $password !== $this->password) {
|
|
$this->username = $username;
|
|
$this->password = $password;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function host(BackedEnum|Stringable|string|null $host): self
|
|
{
|
|
$host = $this->filterString($host);
|
|
if ($host !== $this->host) {
|
|
null === $host
|
|
|| HostRecord::isValid($host)
|
|
|| throw new SyntaxError('The host `'.$host.'` is invalid.');
|
|
|
|
$this->host = $host;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
* @throws TypeError
|
|
*/
|
|
public function port(BackedEnum|int|null $port): self
|
|
{
|
|
if ($port instanceof BackedEnum) {
|
|
1 === preg_match('/^\d+$/', (string) $port->value)
|
|
|| throw new TypeError('The port must be a valid BackedEnum containing a number.');
|
|
|
|
$port = (int) $port->value;
|
|
}
|
|
|
|
if ($port !== $this->port) {
|
|
null === $port
|
|
|| ($port >= 0 && $port < 65535)
|
|
|| throw new SyntaxError('The port value must be null or an integer between 0 and 65535.');
|
|
|
|
$this->port = $port;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function authority(BackedEnum|Stringable|string|null $authority): self
|
|
{
|
|
['user' => $user, 'pass' => $pass, 'host' => $host, 'port' => $port] = UriString::parseAuthority($authority);
|
|
|
|
return $this
|
|
->userInfo($user, $pass)
|
|
->host($host)
|
|
->port($port);
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function path(BackedEnum|Stringable|string|null $path): self
|
|
{
|
|
$path = $this->filterString($path);
|
|
if ($path !== $this->path) {
|
|
$this->path = null !== $path ? Encoder::encodePath($path) : null;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function query(BackedEnum|Stringable|string|null $query): self
|
|
{
|
|
$query = $this->filterString($query);
|
|
if ($query !== $this->query) {
|
|
$this->query = Encoder::encodeQueryOrFragment($query);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
public function fragment(BackedEnum|Stringable|string|null $fragment): self
|
|
{
|
|
$fragment = $this->filterString($fragment);
|
|
if ($fragment !== $this->fragment) {
|
|
$this->fragment = Encoder::encodeQueryOrFragment($fragment);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Puts back the Builder in a freshly created state.
|
|
*/
|
|
public function reset(): self
|
|
{
|
|
$this->scheme = null;
|
|
$this->username = null;
|
|
$this->password = null;
|
|
$this->host = null;
|
|
$this->port = null;
|
|
$this->path = null;
|
|
$this->query = null;
|
|
$this->fragment = null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Executes the given callback with the current instance
|
|
* and returns the current instance.
|
|
*
|
|
* @param callable(self): self $callback
|
|
*/
|
|
public function transform(callable $callback): static
|
|
{
|
|
return $callback($this);
|
|
}
|
|
|
|
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
|
|
{
|
|
if (!is_bool($condition)) {
|
|
$condition = $condition($this);
|
|
}
|
|
|
|
return match (true) {
|
|
$condition => $onSuccess($this),
|
|
null !== $onFail => $onFail($this),
|
|
default => $this,
|
|
} ?? $this;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError if the URI can not be build with the current Builder state
|
|
*/
|
|
public function guard(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): self
|
|
{
|
|
try {
|
|
$this->build($baseUri);
|
|
|
|
return $this;
|
|
} catch (Throwable $exception) {
|
|
throw new SyntaxError('The current builder cannot generate a valid URI.', previous: $exception);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tells whether the URI can be built with the current Builder state.
|
|
*/
|
|
public function validate(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): bool
|
|
{
|
|
try {
|
|
$this->build($baseUri);
|
|
|
|
return true;
|
|
} catch (Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function build(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Uri
|
|
{
|
|
$authority = $this->buildAuthority();
|
|
$path = $this->buildPath($authority);
|
|
$uriString = UriString::buildUri(
|
|
$this->scheme,
|
|
$authority,
|
|
$path,
|
|
Encoder::encodeQueryOrFragment($this->query),
|
|
Encoder::encodeQueryOrFragment($this->fragment)
|
|
);
|
|
|
|
return Uri::new(null === $baseUri ? $uriString : UriString::resolve($uriString, match (true) {
|
|
$baseUri instanceof Rfc3986Uri => $baseUri->toString(),
|
|
$baseUri instanceof WhatWgUrl => $baseUri->toAsciiString(),
|
|
default => $baseUri,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
private function buildAuthority(): ?string
|
|
{
|
|
if (null === $this->host) {
|
|
(null === $this->username && null === $this->password && null === $this->port)
|
|
|| throw new SyntaxError('The User Information and/or the Port component(s) are set without a Host component being present.');
|
|
|
|
return null;
|
|
}
|
|
|
|
$authority = $this->host;
|
|
if (null !== $this->username || null !== $this->password) {
|
|
$userInfo = Encoder::encodeUser($this->username);
|
|
if (null !== $this->password) {
|
|
$userInfo .= ':'.Encoder::encodePassword($this->password);
|
|
}
|
|
|
|
$authority = $userInfo.'@'.$authority;
|
|
}
|
|
|
|
if (null !== $this->port) {
|
|
return $authority.':'.$this->port;
|
|
}
|
|
|
|
return $authority;
|
|
}
|
|
|
|
/**
|
|
* @throws SyntaxError
|
|
*/
|
|
private function buildPath(?string $authority): ?string
|
|
{
|
|
if (null === $this->path || '' === $this->path) {
|
|
return $this->path;
|
|
}
|
|
|
|
$path = Encoder::encodePath($this->path);
|
|
if (null !== $authority) {
|
|
return str_starts_with($path, '/') ? $path : '/'.$path;
|
|
}
|
|
|
|
if (str_starts_with($path, '//')) {
|
|
return '/.'.$path;
|
|
}
|
|
|
|
$colonPos = strpos($path, ':');
|
|
if (false !== $colonPos && null === $this->scheme) {
|
|
$slashPos = strpos($path, '/');
|
|
(false !== $slashPos && $colonPos > $slashPos) || throw new SyntaxError('In absence of the scheme and authority components, the first path segment cannot contain a colon (":") character.');
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Filter a string.
|
|
*
|
|
* @throws SyntaxError if the submitted data cannot be converted to string
|
|
*/
|
|
private function filterString(BackedEnum|Stringable|string|null $str): ?string
|
|
{
|
|
$str = match (true) {
|
|
$str instanceof FragmentDirective => $str->toFragmentValue(),
|
|
$str instanceof UriComponentInterface => $str->value(),
|
|
$str instanceof BackedEnum => (string) $str->value,
|
|
null === $str => null,
|
|
default => (string) $str,
|
|
};
|
|
|
|
if (null === $str) {
|
|
return null;
|
|
}
|
|
|
|
$str = str_replace(' ', '%20', $str);
|
|
|
|
return UriString::containsRfc3987Chars($str)
|
|
? $str
|
|
: throw new SyntaxError('The component value `'.$str.'` contains invalid characters.');
|
|
}
|
|
}
|