-
-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Unify BaseUri feature in version 7.0
- Loading branch information
Showing
9 changed files
with
405 additions
and
363 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,357 @@ | ||
<?php | ||
|
||
/** | ||
* League.Uri (https://uri.thephpleague.com) | ||
* | ||
* (c) Ignace Nyamagana Butera <[email protected]> | ||
* | ||
* 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 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 str_repeat; | ||
use function strpos; | ||
use function substr; | ||
|
||
final class BaseUri | ||
{ | ||
/** | ||
* @var array<string,int> | ||
*/ | ||
private const DOT_SEGMENTS = ['.' => 1, '..' => 1]; | ||
|
||
private function __construct( | ||
public readonly UriInterface $value | ||
) { | ||
} | ||
|
||
public static function new(Stringable|string $baseUri): self | ||
{ | ||
return new self(Uri::new($baseUri)); | ||
} | ||
|
||
/** | ||
* Input URI normalization to allow Stringable and string URI. | ||
*/ | ||
private static function filterUri(Stringable|string $uri): Psr7UriInterface|UriInterface | ||
{ | ||
return match (true) { | ||
$uri instanceof Psr7UriInterface, $uri instanceof UriInterface => $uri, | ||
default => Uri::new($uri), | ||
}; | ||
} | ||
|
||
/** | ||
* 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): Psr7UriInterface|UriInterface | ||
{ | ||
$uri = self::filterUri($uri); | ||
$null = $uri instanceof Psr7UriInterface ? '' : null; | ||
|
||
if ($null !== $uri->getScheme()) { | ||
return $uri | ||
->withPath(self::removeDotSegments($uri->getPath())); | ||
} | ||
|
||
if ($null !== $uri->getAuthority()) { | ||
$scheme = $this->value->getScheme(); | ||
if (null === $scheme || '' === $null) { | ||
$scheme = ''; | ||
} | ||
|
||
return $uri | ||
->withScheme($scheme) | ||
->withPath(self::removeDotSegments($uri->getPath())); | ||
} | ||
|
||
$user = $null; | ||
$pass = null; | ||
$userInfo = $this->value->getUserInfo(); | ||
if (null !== $userInfo) { | ||
[$user, $pass] = explode(':', $userInfo, 2) + [1 => null]; | ||
} | ||
|
||
[$path, $query] = $this->resolvePathAndQuery($uri); | ||
|
||
return $uri | ||
->withPath($this->removeDotSegments($path)) | ||
->withQuery($query) | ||
->withHost($this->value->getHost()) | ||
->withPort($this->value->getPort()) | ||
->withUserInfo((string) $user, $pass) | ||
->withScheme($this->value->getScheme()) | ||
; | ||
} | ||
|
||
/** | ||
* Remove dot segments from the URI path. | ||
*/ | ||
private function removeDotSegments(string $path): string | ||
{ | ||
if (!str_contains($path, '.')) { | ||
return $path; | ||
} | ||
|
||
$oldSegments = explode('/', $path); | ||
$newPath = implode('/', array_reduce($oldSegments, self::reducer(...), [])); | ||
if (isset(self::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; | ||
} | ||
|
||
/** | ||
* 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; | ||
} | ||
|
||
/** | ||
* Resolves an URI path and query component. | ||
* | ||
* @return array{0:string, 1:string|null} | ||
*/ | ||
private function resolvePathAndQuery(Psr7UriInterface|UriInterface $uri): array | ||
{ | ||
$targetPath = $uri->getPath(); | ||
$targetQuery = $uri->getQuery(); | ||
$null = $uri instanceof Psr7UriInterface ? '' : null; | ||
$baseNull = $this->value instanceof Psr7UriInterface ? '' : null; | ||
|
||
if (str_starts_with($targetPath, '/')) { | ||
return [$targetPath, $targetQuery]; | ||
} | ||
|
||
if ('' === $targetPath) { | ||
if ($null === $targetQuery) { | ||
$targetQuery = $this->value->getQuery(); | ||
} | ||
|
||
$targetPath = $this->value->getPath(); | ||
//@codeCoverageIgnoreStart | ||
//because some PSR-7 Uri implementations allow this RFC3986 forbidden construction | ||
if ($baseNull !== $this->value->getAuthority() && !str_starts_with($targetPath, '/')) { | ||
$targetPath = '/'.$targetPath; | ||
} | ||
//@codeCoverageIgnoreEnd | ||
|
||
return [$targetPath, $targetQuery]; | ||
} | ||
|
||
$basePath = $this->value->getPath(); | ||
if ($baseNull !== $this->value->getAuthority() && '' === $basePath) { | ||
$targetPath = '/'.$targetPath; | ||
} | ||
|
||
if ('' !== $basePath) { | ||
$segments = explode('/', $basePath); | ||
array_pop($segments); | ||
if ([] !== $segments) { | ||
$targetPath = implode('/', $segments).'/'.$targetPath; | ||
} | ||
} | ||
|
||
return [$targetPath, $targetQuery]; | ||
} | ||
|
||
/** | ||
* Relativizes a 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. | ||
*/ | ||
public function relativize(Stringable|string $uri): Psr7UriInterface|UriInterface | ||
{ | ||
$uri = self::formatHost(self::filterUri($uri)); | ||
if (!$this->isRelativizable($uri)) { | ||
return $uri; | ||
} | ||
|
||
$null = $uri instanceof Psr7UriInterface ? '' : null; | ||
$uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null); | ||
$targetPath = $uri->getPath(); | ||
$basePath = $this->value->getPath(); | ||
if ($targetPath !== $basePath) { | ||
return $uri->withPath(self::relativizePath($targetPath, $basePath)); | ||
} | ||
|
||
if (self::componentEquals('query', $uri)) { | ||
return $uri->withPath('')->withQuery($null); | ||
} | ||
|
||
if ($null === $uri->getQuery()) { | ||
return $uri->withPath(self::formatPathWithEmptyBaseQuery($targetPath)); | ||
} | ||
|
||
return $uri->withPath(''); | ||
} | ||
|
||
/** | ||
* Tells whether the component value from both URI object equals. | ||
*/ | ||
private function componentEquals(string $property, Psr7UriInterface|UriInterface $uri): bool | ||
{ | ||
return self::getComponent($property, $uri) === self::getComponent($property, $this->value); | ||
} | ||
|
||
/** | ||
* Returns the component value from the submitted URI object. | ||
*/ | ||
private static function getComponent(string $property, Psr7UriInterface|UriInterface $uri): ?string | ||
{ | ||
$component = match ($property) { | ||
'query' => $uri->getQuery(), | ||
'authority' => $uri->getAuthority(), | ||
default => $uri->getScheme(), //scheme | ||
}; | ||
|
||
if ($uri instanceof Psr7UriInterface && '' === $component) { | ||
return null; | ||
} | ||
|
||
return $component; | ||
} | ||
|
||
/** | ||
* Filter the URI object. | ||
*/ | ||
private static function formatHost(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface | ||
{ | ||
if (!$uri instanceof Psr7UriInterface) { | ||
return $uri; | ||
} | ||
|
||
$host = $uri->getHost(); | ||
if ('' === $host) { | ||
return $uri; | ||
} | ||
|
||
return $uri->withHost((string) Uri::fromComponents(['host' => $host])->getHost()); | ||
} | ||
|
||
/** | ||
* Tells whether the submitted URI object can be relativized. | ||
*/ | ||
private function isRelativizable(Psr7UriInterface|UriInterface $uri): bool | ||
{ | ||
return !UriInfo::isRelativePath($uri) | ||
&& self::componentEquals('scheme', $uri) | ||
&& self::componentEquals('authority', $uri); | ||
} | ||
|
||
/** | ||
* Relatives the URI for an authority-less target URI. | ||
*/ | ||
private static function relativizePath(string $path, string $basePath): string | ||
{ | ||
$baseSegments = self::getSegments($basePath); | ||
$targetSegments = self::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 self::formatPath( | ||
str_repeat('../', count($baseSegments)).implode('/', $targetSegments), | ||
$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 === ($colonPosition = strpos($path, ':'))) { | ||
return $path; | ||
} | ||
|
||
$slashPosition = strpos($path, '/'); | ||
if (false === $slashPosition || $colonPosition < $slashPosition) { | ||
return "./$path"; | ||
} | ||
|
||
return $path; | ||
} | ||
|
||
/** | ||
* Formatting the path to keep a resolvable URI. | ||
*/ | ||
private static function formatPathWithEmptyBaseQuery(string $path): string | ||
{ | ||
$targetSegments = self::getSegments($path); | ||
/** @var string $basename */ | ||
$basename = end($targetSegments); | ||
|
||
return '' === $basename ? './' : $basename; | ||
} | ||
} |
Oops, something went wrong.