From cfeec96df752a520a51de0c4681e8c0954bb0b58 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 29 Jun 2023 17:32:15 +0200 Subject: [PATCH] Unify BaseUri feature in version 7.0 --- BaseUri.php | 357 +++++++++++++++++++++++++ UriResolverTest.php => BaseUriTest.php | 31 +-- CHANGELOG.md | 8 +- FactoryTest.php | 10 +- Http.php | 8 +- HttpTest.php | 2 +- Uri.php | 19 +- UriInfo.php | 2 +- UriResolver.php | 331 +---------------------- 9 files changed, 405 insertions(+), 363 deletions(-) create mode 100644 BaseUri.php rename UriResolverTest.php => BaseUriTest.php (91%) diff --git a/BaseUri.php b/BaseUri.php new file mode 100644 index 00000000..973536f3 --- /dev/null +++ b/BaseUri.php @@ -0,0 +1,357 @@ + + * + * 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 + */ + 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 + */ + 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; + } +} diff --git a/UriResolverTest.php b/BaseUriTest.php similarity index 91% rename from UriResolverTest.php rename to BaseUriTest.php index 897a593d..e9bd8aae 100644 --- a/UriResolverTest.php +++ b/BaseUriTest.php @@ -15,29 +15,18 @@ /** * @group modifier - * @coversDefaultClass \League\Uri\UriResolver + * @coversDefaultClass \League\Uri\BaseUri */ -final class UriResolverTest extends TestCase +final class BaseUriTest extends TestCase { private const BASE_URI = 'http://a/b/c/d;p?q'; - public function testResolveLetThrowResolvedInvalidUri(): void - { - $http = Uri::new('https://example.com/path/to/file'); - $ftp = Http::new('ftp://a/b/c/d;p'); - - self::assertEquals(UriResolver::resolve($ftp, $http), $ftp); - } - /** * @dataProvider resolveProvider */ - public function testCreateResolve(string $base_uri, string $uri, string $expected): void + public function testCreateResolve(string $baseUri, string $uri, string $expected): void { - self::assertSame($expected, (string) UriResolver::resolve( - Uri::new($uri), - $base_uri - )); + self::assertSame($expected, (string) BaseUri::new($baseUri)->resolve($uri)); } public static function resolveProvider(): array @@ -80,16 +69,16 @@ public static function resolveProvider(): array 'dot segments presence 4' => [self::BASE_URI, '.g', 'http://a/b/c/.g'], 'dot segments presence 5' => [self::BASE_URI, 'g..', 'http://a/b/c/g..'], 'dot segments presence 6' => [self::BASE_URI, '..g', 'http://a/b/c/..g'], - 'origin uri without path' => ['http://h:b@a', 'b/../y', 'http://h:b@a/y'], + 'origin uri without path' => ['http://h:b@a', 'b/../y', 'http://h:b@a/y'], + 'not same origin' => [self::BASE_URI, 'ftp://a/b/c/d', 'ftp://a/b/c/d'], ]; } public function testRelativizeIsNotMade(): void { - $uri = Uri::new('//path#fragment'); - $base_uri = Http::new('https://example.com/path'); + $uri = '//path#fragment'; - self::assertEquals($uri, UriResolver::relativize($uri, $base_uri)); + self::assertEquals($uri, (string) BaseUri::new('https://example.com/path')->relativize($uri)); } /** @@ -99,7 +88,7 @@ public function testRelativize(string $uri, string $resolved, string $expected): { self::assertSame( $expected, - (string) UriResolver::relativize(Uri::new($resolved), Http::new($uri)) + (string) BaseUri::new(Http::new($uri))->relativize($resolved) ); } @@ -156,7 +145,7 @@ public function testRelativizeAndResolve( ): void { self::assertSame( $expectedRelativize, - (string) UriResolver::relativize($uri, Uri::new($baseUri)) + (string) BaseUri::new($baseUri)->relativize($uri) ); } diff --git a/CHANGELOG.md b/CHANGELOG.md index e11609d6..49d007b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,19 +23,21 @@ All Notable changes to `League\Uri` will be documented in this file - `League\Uri\UriTemplate\Template::expandOrFail` - `League\Uri\UriString::parseAuthority` - `League\Uri\UriString::buildAuthority` +- `League\Uri\BaseUri` ### Fixed -- `League\Uri\UriResolver` and `League\Uri\UriInfo` uri input now supports `Stringable` and `string` type. +- `League\Uri\UriInfo` uri input now supports `Stringable` and `string` type. - `League\Uri\UriTemplate\VariableBag` implements the `IteratorAggregate` interface - `League\Uri\UriTemplate\Operator` to improve internal representation when using UriTemplate features. ### Deprecated +- `League\Uri\UriResolver` use `League\Uri\BaseUri` instead - `League\Uri\Uri::createFromString` use `League\Uri\Uri::new` - `League\Uri\Uri::createFromUri` use `League\Uri\Uri::new` - `League\Uri\Uri::createFromComponents` use `League\Uri\Uri::fromComponents` -- `League\Uri\Uri::createFromBaseUri` use `League\Uri\Uri::fromClient` +- `League\Uri\Uri::createFromBaseUri` use `League\Uri\Uri::fromBaseUri` - `League\Uri\Uri::createFromServer` use `League\Uri\Uri::fromServer` - `League\Uri\Uri::createFromWindowsPath` use `League\Uri\Uri::fromWindowsPath` - `League\Uri\Uri::createFromUnixPath` use `League\Uri\Uri::fromUnixPath` @@ -43,7 +45,7 @@ All Notable changes to `League\Uri` will be documented in this file - `League\Uri\Http::createFromString` use `League\Uri\Http::new` - `League\Uri\Http::createFromUri` use `League\Uri\Http::new` - `League\Uri\Http::createFromComponents` use `League\Uri\Http::fromComponents` -- `League\Uri\Http::createFromBaseUri` use `League\Uri\Http::fromClient` +- `League\Uri\Http::createFromBaseUri` use `League\Uri\Http::fromBaseUri` - `League\Uri\Http::createFromServer` use `League\Uri\Http::fromServer` - `League\Uri\UriTemplate\Template::createFromString` use `League\Uri\UriTemplate\Template::new` diff --git a/FactoryTest.php b/FactoryTest.php index 47dbe3cb..aa704e7a 100644 --- a/FactoryTest.php +++ b/FactoryTest.php @@ -339,7 +339,7 @@ public function testFailCreateFromServerWithoutInvalidUserInfo(): void */ public function testCreateFromBaseUri(string $base_uri, string $uri, string $expected): void { - self::assertSame($expected, Uri::fromClient($uri, $base_uri)->toString()); + self::assertSame($expected, Uri::fromBaseUri($uri, $base_uri)->toString()); } public static function createProvider(): array @@ -392,20 +392,20 @@ public static function createProvider(): array public function testCreateThrowExceptionWithBaseUriNotAbsolute(): void { self::expectException(SyntaxError::class); - Uri::fromClient('/path/to/you', '//example.com'); + Uri::fromBaseUri('/path/to/you', '//example.com'); } public function testCreateThrowExceptionWithUriNotAbsolute(): void { self::expectException(SyntaxError::class); - Uri::fromClient('/path/to/you'); + Uri::fromBaseUri('/path/to/you'); } public function testCreateWithUriWithoutAuthority(): void { self::assertSame( 'data:text/plain;charset=us-ascii,', - Uri::fromClient('data:text/plain;charset=us-ascii,')->toString() + Uri::fromBaseUri('data:text/plain;charset=us-ascii,')->toString() ); } @@ -413,7 +413,7 @@ public function testCreateWithAbsoluteUriWithoutBaseUri(): void { self::assertSame( 'scheme://host/sky?q#f', - Uri::fromClient('scheme://host/path/../sky?q#f')->toString() + Uri::fromBaseUri('scheme://host/path/../sky?q#f')->toString() ); } diff --git a/Http.php b/Http.php index e6afc467..f7d29f1d 100644 --- a/Http.php +++ b/Http.php @@ -81,9 +81,9 @@ public static function fromServer(array $server): self * * The returned URI must be absolute. */ - public static function fromClient(Stringable|String $uri, Stringable|String|null $baseUri = null): self + public static function fromBaseUri(Stringable|String $uri, Stringable|String|null $baseUri = null): self { - return new self(Uri::fromClient($uri, $baseUri)); + return new self(Uri::fromBaseUri($uri, $baseUri)); } public static function fromTemplate(Stringable|string $template, iterable $variables = []): self @@ -259,7 +259,7 @@ public static function createFromUri(Stringable|string $uri): self * * @deprecated Since version 7.0.0 * @codeCoverageIgnore - * @see Http::fromClient() + * @see Http::fromBaseUri() * * Create a new instance from a URI and a Base URI. * @@ -267,6 +267,6 @@ public static function createFromUri(Stringable|string $uri): self */ public static function createFromBaseUri(Stringable|String $uri, Stringable|String|null $baseUri = null): self { - return self::fromClient($uri, $baseUri); + return self::fromBaseUri($uri, $baseUri); } } diff --git a/HttpTest.php b/HttpTest.php index 1af7a802..4d925f43 100644 --- a/HttpTest.php +++ b/HttpTest.php @@ -90,7 +90,7 @@ public function testCreateFromBaseUri(): void { self::assertEquals( Http::new('http://0:0@0/0?0#0'), - Http::fromClient('0?0#0', 'http://0:0@0/') + Http::fromBaseUri('0?0#0', 'http://0:0@0/') ); } diff --git a/Uri.php b/Uri.php index b30823f3..78b5fedd 100644 --- a/Uri.php +++ b/Uri.php @@ -431,7 +431,7 @@ public static function new(Stringable|string $uri = ''): self * * The returned URI must be absolute. */ - public static function fromClient(Stringable|String $uri, Stringable|String|null $baseUri = null): UriInterface + public static function fromBaseUri(Stringable|String $uri, Stringable|String|null $baseUri = null): UriInterface { if (!$uri instanceof UriInterface) { $uri = self::new($uri); @@ -447,21 +447,18 @@ public static function fromClient(Stringable|String $uri, Stringable|String|null } /** @var UriInterface $uri */ - $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath('')); + $uri = BaseUri::new($uri->withFragment(null)->withQuery(null)->withPath(''))->resolve($uri); return $uri; } - if (!$baseUri instanceof UriInterface) { - $baseUri = self::new($baseUri); - } - - if (null === $baseUri->getScheme()) { - throw new SyntaxError('the base URI `'.$baseUri.'` must be absolute.'); + $baseUri = BaseUri::new($baseUri); + if (null === $baseUri->value->getScheme()) { + throw new SyntaxError('the base URI `'.$baseUri->value.'` must be absolute.'); } /** @var UriInterface $uri */ - $uri = UriResolver::resolve($uri, $baseUri); + $uri = $baseUri->resolve($uri); return $uri; } @@ -1295,7 +1292,7 @@ public static function createFromDataPath(string $path, $context = null): self * * @deprecated Since version 7.0.0 * @codeCoverageIgnore - * @see Uri::fromClient() + * @see Uri::fromBaseUri() * * Creates a new instance from a URI and a Base URI. * @@ -1305,7 +1302,7 @@ public static function createFromBaseUri( Stringable|UriInterface|String $uri, Stringable|UriInterface|String|null $baseUri = null ): UriInterface { - return self::fromClient($uri, $baseUri); + return self::fromBaseUri($uri, $baseUri); } /** diff --git a/UriInfo.php b/UriInfo.php index 47c5afef..754c26ba 100644 --- a/UriInfo.php +++ b/UriInfo.php @@ -58,7 +58,7 @@ private static function normalize(Psr7UriInterface|UriInterface $uri): Psr7UriIn $path = $uri->getPath(); if ('/' === ($path[0] ?? '') || '' !== $uri->getScheme().$uri->getAuthority()) { - $path = UriResolver::resolve($uri, $uri->withPath('')->withQuery($null))->getPath(); + $path = BaseUri::new($uri->withPath('')->withQuery($null))->resolve($uri)->getPath(); } $query = $uri->getQuery(); diff --git a/UriResolver.php b/UriResolver.php index dc1fc734..de980936 100644 --- a/UriResolver.php +++ b/UriResolver.php @@ -16,342 +16,39 @@ 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; +/** + * @deprecated since version 7.0.0 + * @codeCoverageIgnore + * @see BaseUri + */ final class UriResolver { - /** - * @var array - */ - const DOT_SEGMENTS = ['.' => 1, '..' => 1]; - - /** - * @codeCoverageIgnore - */ - private function __construct() - { - } - - /** - * 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. * - * 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 - */ - public static function resolve(Stringable|string $uri, Stringable|string $baseUri): Psr7UriInterface|UriInterface - { - $uri = self::filterUri($uri); - $baseUri = self::filterUri($baseUri); - $null = $uri instanceof Psr7UriInterface ? '' : null; - - if ($null !== $uri->getScheme()) { - return $uri - ->withPath(self::removeDotSegments($uri->getPath())); - } - - if ($null !== $uri->getAuthority()) { - return $uri - ->withScheme($baseUri->getScheme()) - ->withPath(self::removeDotSegments($uri->getPath())); - } - - $user = $null; - $pass = null; - $userInfo = $baseUri->getUserInfo(); - if (null !== $userInfo) { - [$user, $pass] = explode(':', $userInfo, 2) + [1 => null]; - } - - [$path, $query] = self::resolvePathAndQuery($uri, $baseUri); - - return $uri - ->withPath(self::removeDotSegments($path)) - ->withQuery($query) - ->withHost($baseUri->getHost()) - ->withPort($baseUri->getPort()) - ->withUserInfo((string) $user, $pass) - ->withScheme($baseUri->getScheme()) - ; - } - - /** - * Remove dot segments from the URI path. - */ - private static function removeDotSegments(string $path): string - { - if (!str_contains($path, '.')) { - return $path; - } - - $oldSegments = explode('/', $path); - $new_path = implode('/', array_reduce($oldSegments, UriResolver::reducer(...), [])); - if (isset(self::DOT_SEGMENTS[end($oldSegments)])) { - $new_path .= '/'; - } - - // @codeCoverageIgnoreStart - // added because some PSR-7 implementations do not respect RFC3986 - if (str_starts_with($path, '/') && !str_starts_with($new_path, '/')) { - return '/'.$new_path; - } - // @codeCoverageIgnoreEnd - - return $new_path; - } - - /** - * Remove dot segments. + * 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. * - * @return array + * This method MUST be transparent when dealing with error and exceptions. + * It MUST not alter or silence them apart from validating its own parameters. */ - private static function reducer(array $carry, string $segment): array + public static function resolve(Stringable|string $uri, Stringable|string $baseUri): Psr7UriInterface|UriInterface { - 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 static function resolvePathAndQuery( - Psr7UriInterface|UriInterface $uri, - Psr7UriInterface|UriInterface $baseUri - ): array { - $targetPath = $uri->getPath(); - $targetQuery = $uri->getQuery(); - $null = $uri instanceof Psr7UriInterface ? '' : null; - $baseNull = $baseUri instanceof Psr7UriInterface ? '' : null; - - if (str_starts_with($targetPath, '/')) { - return [$targetPath, $targetQuery]; - } - - if ('' === $targetPath) { - if ($null === $targetQuery) { - $targetQuery = $baseUri->getQuery(); - } - - $targetPath = $baseUri->getPath(); - //@codeCoverageIgnoreStart - //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction - if ($baseNull !== $baseUri->getAuthority() && !str_starts_with($targetPath, '/')) { - $targetPath = '/'.$targetPath; - } - //@codeCoverageIgnoreEnd - - return [$targetPath, $targetQuery]; - } - - $base_path = $baseUri->getPath(); - if ($baseNull !== $baseUri->getAuthority() && '' === $base_path) { - $targetPath = '/'.$targetPath; - } - - if ('' !== $base_path) { - $segments = explode('/', $base_path); - array_pop($segments); - if ([] !== $segments) { - $targetPath = implode('/', $segments).'/'.$targetPath; - } - } - - return [$targetPath, $targetQuery]; + return BaseUri::new($baseUri)->resolve($uri); } /** * 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. + * 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. + * It MUST not alter or silence them apart from validating its own parameters. */ public static function relativize(Stringable|string $uri, Stringable|string $baseUri): Psr7UriInterface|UriInterface { - $uri = self::filterUri($uri); - $baseUri = self::filterUri($baseUri); - - $uri = self::formatHost($uri); - $baseUri = self::formatHost($baseUri); - if (!self::isRelativizable($uri, $baseUri)) { - return $uri; - } - - $null = $uri instanceof Psr7UriInterface ? '' : null; - $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null); - $targetPath = $uri->getPath(); - if ($targetPath !== $baseUri->getPath()) { - return $uri->withPath(self::relativizePath($targetPath, $baseUri->getPath())); - } - - if (self::componentEquals('query', $uri, $baseUri)) { - 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 static function componentEquals( - string $property, - Psr7UriInterface|UriInterface $uri, - Psr7UriInterface|UriInterface $baseUri - ): bool { - return self::getComponent($property, $uri) === self::getComponent($property, $baseUri); - } - - /** - * 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 static function isRelativizable( - Psr7UriInterface|UriInterface $uri, - Psr7UriInterface|UriInterface $baseUri - ): bool { - return !UriInfo::isRelativePath($uri) - && self::componentEquals('scheme', $uri, $baseUri) - && self::componentEquals('authority', $uri, $baseUri); - } - - /** - * 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; + return BaseUri::new($baseUri)->relativize($uri); } }