You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
10 KiB
358 lines
10 KiB
<?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.'); |
|
} |
|
}
|
|
|