Viewing file: BaseUri.php (20.63 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?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 JsonSerializable; use League\Uri\Contracts\UriAccess; use League\Uri\Contracts\UriInterface; use League\Uri\Exceptions\MissingFeature; use League\Uri\Idna\Converter as IdnaConverter; use League\Uri\IPv4\Converter as IPv4Converter; use League\Uri\IPv6\Converter as IPv6Converter; use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface as Psr7UriInterface; use Stringable;
use function array_pop; use function array_reduce; use function count; use function end; use function explode; use function implode; use function in_array; use function preg_match; use function rawurldecode; use function str_repeat; use function str_replace; use function strpos; use function substr;
/** * @phpstan-import-type ComponentMap from UriInterface */ class BaseUri implements Stringable, JsonSerializable, UriAccess { /** @var array<string,int> */ final protected const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1];
/** @var array<string,int> */ final protected const DOT_SEGMENTS = ['.' => 1, '..' => 1];
protected readonly Psr7UriInterface|UriInterface|null $origin; protected readonly ?string $nullValue;
/** * @param UriFactoryInterface|null $uriFactory Deprecated, will be removed in the next major release */ final protected function __construct( protected readonly Psr7UriInterface|UriInterface $uri, protected readonly ?UriFactoryInterface $uriFactory ) { $this->nullValue = $this->uri instanceof Psr7UriInterface ? '' : null; $this->origin = $this->computeOrigin($this->uri, $this->nullValue); }
public static function from(Stringable|string $uri, ?UriFactoryInterface $uriFactory = null): static { return new static(static::formatHost(static::filterUri($uri, $uriFactory)), $uriFactory); }
public function withUriFactory(UriFactoryInterface $uriFactory): static { return new static($this->uri, $uriFactory); }
public function withoutUriFactory(): static { return new static($this->uri, null); }
public function getUri(): Psr7UriInterface|UriInterface { return $this->uri; }
public function getUriString(): string { return $this->uri->__toString(); }
public function jsonSerialize(): string { return $this->uri->__toString(); }
public function __toString(): string { return $this->uri->__toString(); }
public function origin(): ?self { return match (null) { $this->origin => null, default => new self($this->origin, $this->uriFactory), }; }
/** * Returns the Unix filesystem path. * * The method will return null if a scheme is present and is not the `file` scheme */ public function unixPath(): ?string { return match ($this->uri->getScheme()) { 'file', $this->nullValue => rawurldecode($this->uri->getPath()), default => null, }; }
/** * Returns the Windows filesystem path. * * The method will return null if a scheme is present and is not the `file` scheme */ public function windowsPath(): ?string { static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),';
if (!in_array($this->uri->getScheme(), ['file', $this->nullValue], true)) { return null; }
$originalPath = $this->uri->getPath(); $path = $originalPath; if ('/' === ($path[0] ?? '')) { $path = substr($path, 1); }
if (1 === preg_match($regexpWindowsPath, $path, $matches)) { $root = $matches['root']; $path = substr($path, strlen($root));
return $root.str_replace('/', '\\', rawurldecode($path)); }
$host = $this->uri->getHost();
return match ($this->nullValue) { $host => str_replace('/', '\\', rawurldecode($originalPath)), default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)), }; }
/** * Returns a string representation of a File URI according to RFC8089. * * The method will return null if the URI scheme is not the `file` scheme */ public function toRfc8089(): ?string { $path = $this->uri->getPath();
return match (true) { 'file' !== $this->uri->getScheme() => null, in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => 'file:'.match (true) { '' === $path, '/' === $path[0] => $path, default => '/'.$path, }, default => (string) $this->uri, }; }
/** * Tells whether the `file` scheme base URI represents a local file. */ public function isLocalFile(): bool { return match (true) { 'file' !== $this->uri->getScheme() => false, in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => true, default => false, }; }
/** * Tells whether the URI is opaque or not. * * A URI is opaque if and only if it is absolute * and does not has an authority path. */ public function isOpaque(): bool { return $this->nullValue === $this->uri->getAuthority() && $this->isAbsolute(); }
/** * Tells whether two URI do not share the same origin. */ public function isCrossOrigin(Stringable|string $uri): bool { if (null === $this->origin) { return true; }
$uri = static::filterUri($uri); $uriOrigin = $this->computeOrigin($uri, $uri instanceof Psr7UriInterface ? '' : null);
return match(true) { null === $uriOrigin, $uriOrigin->__toString() !== $this->origin->__toString() => true, default => false, }; }
/** * Tells whether the URI is absolute. */ public function isAbsolute(): bool { return $this->nullValue !== $this->uri->getScheme(); }
/** * Tells whether the URI is a network path. */ public function isNetworkPath(): bool { return $this->nullValue === $this->uri->getScheme() && $this->nullValue !== $this->uri->getAuthority(); }
/** * Tells whether the URI is an absolute path. */ public function isAbsolutePath(): bool { return $this->nullValue === $this->uri->getScheme() && $this->nullValue === $this->uri->getAuthority() && '/' === ($this->uri->getPath()[0] ?? ''); }
/** * Tells whether the URI is a relative path. */ public function isRelativePath(): bool { return $this->nullValue === $this->uri->getScheme() && $this->nullValue === $this->uri->getAuthority() && '/' !== ($this->uri->getPath()[0] ?? ''); }
/** * Tells whether both URI refers to the same document. */ public function isSameDocument(Stringable|string $uri): bool { return $this->normalize(static::filterUri($uri)) === $this->normalize($this->uri); }
/** * Tells whether the URI contains an Internationalized Domain Name (IDN). */ public function hasIdn(): bool { return IdnaConverter::isIdn($this->uri->getHost()); }
/** * Tells whether the URI contains an IPv4 regardless if it is mapped or native. */ public function hasIPv4(): bool { return IPv4Converter::fromEnvironment()->isIpv4($this->uri->getHost()); }
/** * Resolves a URI against a base URI using RFC3986 rules. * * This method MUST retain the state of the submitted URI instance, and return * a 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 or silence them apart from validating its own parameters. */ public function resolve(Stringable|string $uri): static { $uri = static::formatHost(static::filterUri($uri, $this->uriFactory)); $null = $uri instanceof Psr7UriInterface ? '' : null;
if ($null !== $uri->getScheme()) { return new static( $uri->withPath(static::removeDotSegments($uri->getPath())), $this->uriFactory ); }
if ($null !== $uri->getAuthority()) { return new static( $uri ->withScheme($this->uri->getScheme()) ->withPath(static::removeDotSegments($uri->getPath())), $this->uriFactory ); }
$user = $null; $pass = null; $userInfo = $this->uri->getUserInfo(); if (null !== $userInfo) { [$user, $pass] = explode(':', $userInfo, 2) + [1 => null]; }
[$path, $query] = $this->resolvePathAndQuery($uri);
return new static( $uri ->withPath($this->removeDotSegments($path)) ->withQuery($query) ->withHost($this->uri->getHost()) ->withPort($this->uri->getPort()) ->withUserInfo($user, $pass) ->withScheme($this->uri->getScheme()), $this->uriFactory ); }
/** * Relativize a URI according to a base URI. * * This method MUST retain the state of the submitted URI instance, and return * a 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. */ public function relativize(Stringable|string $uri): static { $uri = static::formatHost(static::filterUri($uri, $this->uriFactory)); if ($this->canNotBeRelativize($uri)) { return new static($uri, $this->uriFactory); }
$null = $uri instanceof Psr7UriInterface ? '' : null; $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null); $targetPath = $uri->getPath(); $basePath = $this->uri->getPath();
return new static( match (true) { $targetPath !== $basePath => $uri->withPath(static::relativizePath($targetPath, $basePath)), static::componentEquals('query', $uri) => $uri->withPath('')->withQuery($null), $null === $uri->getQuery() => $uri->withPath(static::formatPathWithEmptyBaseQuery($targetPath)), default => $uri->withPath(''), }, $this->uriFactory ); }
final protected function computeOrigin(Psr7UriInterface|UriInterface $uri, ?string $nullValue): Psr7UriInterface|UriInterface|null { $scheme = $uri->getScheme(); if ('blob' !== $scheme) { return match (true) { isset(static::WHATWG_SPECIAL_SCHEMES[$scheme]) => $uri ->withFragment($nullValue) ->withQuery($nullValue) ->withPath('') ->withUserInfo($nullValue), default => null, }; }
$components = UriString::parse($uri->getPath()); if ($uri instanceof Psr7UriInterface) { /** @var ComponentMap $components */ $components = array_map(fn ($component) => null === $component ? '' : $component, $components); }
return match (true) { null !== $components['scheme'] && isset(static::WHATWG_SPECIAL_SCHEMES[strtolower($components['scheme'])]) => $uri ->withFragment($nullValue) ->withQuery($nullValue) ->withPath('') ->withHost($components['host']) ->withPort($components['port']) ->withScheme($components['scheme']) ->withUserInfo($nullValue), default => null, }; }
/** * Normalizes a URI for comparison; this URI string representation is not suitable for usage as per RFC guidelines. */ final protected function normalize(Psr7UriInterface|UriInterface $uri): string { $null = $uri instanceof Psr7UriInterface ? '' : null;
$path = $uri->getPath(); if ('/' === ($path[0] ?? '') || '' !== $uri->getScheme().$uri->getAuthority()) { $path = $this->removeDotSegments($path); }
$query = $uri->getQuery(); $pairs = null === $query ? [] : explode('&', $query); sort($pairs);
static $regexpEncodedChars = ',%(2[D|E]|3\d|4[1-9|A-F]|5[\d|AF]|6[1-9|A-F]|7[\d|E]),i'; $value = preg_replace_callback( $regexpEncodedChars, static fn (array $matches): string => rawurldecode($matches[0]), [$path, implode('&', $pairs)] ) ?? ['', $null];
[$path, $query] = $value + ['', $null]; if ($null !== $uri->getAuthority() && '' === $path) { $path = '/'; }
return $uri ->withHost(Uri::fromComponents(['host' => $uri->getHost()])->getHost()) ->withPath($path) ->withQuery([] === $pairs ? $null : $query) ->withFragment($null) ->__toString(); }
/** * Input URI normalization to allow Stringable and string URI. */ final protected static function filterUri(Stringable|string $uri, UriFactoryInterface|null $uriFactory = null): Psr7UriInterface|UriInterface { return match (true) { $uri instanceof UriAccess => $uri->getUri(), $uri instanceof Psr7UriInterface, $uri instanceof UriInterface => $uri, $uriFactory instanceof UriFactoryInterface => $uriFactory->createUri((string) $uri), default => Uri::new($uri), }; }
/** * Remove dot segments from the URI path as per RFC specification. */ final protected function removeDotSegments(string $path): string { if (!str_contains($path, '.')) { return $path; }
$reducer = function (array $carry, string $segment): array { if ('..' === $segment) { array_pop($carry);
return $carry; }
if (!isset(static::DOT_SEGMENTS[$segment])) { $carry[] = $segment; }
return $carry; };
$oldSegments = explode('/', $path); $newPath = implode('/', array_reduce($oldSegments, $reducer(...), [])); if (isset(static::DOT_SEGMENTS[end($oldSegments)])) { $newPath .= '/'; }
// @codeCoverageIgnoreStart // added because some PSR-7 implementations do not respect RFC3986 if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) { return '/'.$newPath; } // @codeCoverageIgnoreEnd
return $newPath; }
/** * Resolves an URI path and query component. * * @return array{0:string, 1:string|null} */ final protected function resolvePathAndQuery(Psr7UriInterface|UriInterface $uri): array { $targetPath = $uri->getPath(); $null = $uri instanceof Psr7UriInterface ? '' : null;
if (str_starts_with($targetPath, '/')) { return [$targetPath, $uri->getQuery()]; }
if ('' === $targetPath) { $targetQuery = $uri->getQuery(); if ($null === $targetQuery) { $targetQuery = $this->uri->getQuery(); }
$targetPath = $this->uri->getPath(); //@codeCoverageIgnoreStart //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction if (null !== $this->uri->getAuthority() && !str_starts_with($targetPath, '/')) { $targetPath = '/'.$targetPath; } //@codeCoverageIgnoreEnd
return [$targetPath, $targetQuery]; }
$basePath = $this->uri->getPath(); if (null !== $this->uri->getAuthority() && '' === $basePath) { $targetPath = '/'.$targetPath; }
if ('' !== $basePath) { $segments = explode('/', $basePath); array_pop($segments); if ([] !== $segments) { $targetPath = implode('/', $segments).'/'.$targetPath; } }
return [$targetPath, $uri->getQuery()]; }
/** * Tells whether the component value from both URI object equals. * * @pqram 'query'|'authority'|'scheme' $property */ final protected function componentEquals(string $property, Psr7UriInterface|UriInterface $uri): bool { $getComponent = function (string $property, Psr7UriInterface|UriInterface $uri): ?string { $component = match ($property) { 'query' => $uri->getQuery(), 'authority' => $uri->getAuthority(), default => $uri->getScheme(), };
return match (true) { $uri instanceof UriInterface, '' !== $component => $component, default => null, }; };
return $getComponent($property, $uri) === $getComponent($property, $this->uri); }
/** * Filter the URI object. */ final protected static function formatHost(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface { $host = $uri->getHost(); try { $converted = IPv4Converter::fromEnvironment()->toDecimal($host); } catch (MissingFeature) { $converted = null; }
if (false === filter_var($converted, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $converted = IPv6Converter::compress($host); }
return match (true) { null !== $converted => $uri->withHost($converted), '' === $host, $uri instanceof UriInterface => $uri, default => $uri->withHost((string) Uri::fromComponents(['host' => $host])->getHost()), }; }
/** * Tells whether the submitted URI object can be relativized. */ final protected function canNotBeRelativize(Psr7UriInterface|UriInterface $uri): bool { return !static::componentEquals('scheme', $uri) || !static::componentEquals('authority', $uri) || static::from($uri)->isRelativePath(); }
/** * Relatives the URI for an authority-less target URI. */ final protected static function relativizePath(string $path, string $basePath): string { $baseSegments = static::getSegments($basePath); $targetSegments = static::getSegments($path); $targetBasename = array_pop($targetSegments); array_pop($baseSegments); foreach ($baseSegments as $offset => $segment) { if (!isset($targetSegments[$offset]) || $segment !== $targetSegments[$offset]) { break; } unset($baseSegments[$offset], $targetSegments[$offset]); } $targetSegments[] = $targetBasename;
return static::formatPath( str_repeat('../', count($baseSegments)).implode('/', $targetSegments), $basePath ); }
/** * returns the path segments. * * @return string[] */ final protected static function getSegments(string $path): array { return explode('/', match (true) { '' === $path, '/' !== $path[0] => $path, default => substr($path, 1), }); }
/** * Formatting the path to keep a valid URI. */ final protected static function formatPath(string $path, string $basePath): string { $colonPosition = strpos($path, ':'); $slashPosition = strpos($path, '/');
return match (true) { '' === $path => match (true) { '' === $basePath, '/' === $basePath => $basePath, default => './', }, false === $colonPosition => $path, false === $slashPosition, $colonPosition < $slashPosition => "./$path", default => $path, }; }
/** * Formatting the path to keep a resolvable URI. */ final protected static function formatPathWithEmptyBaseQuery(string $path): string { $targetSegments = static::getSegments($path); /** @var string $basename */ $basename = end($targetSegments);
return '' === $basename ? './' : $basename; } }
|