Skip to content

Commit

Permalink
Deprecate UriInfo class
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jul 2, 2023
1 parent 4f963cf commit 4059b3b
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 351 deletions.
127 changes: 110 additions & 17 deletions BaseUri.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,41 @@

final class BaseUri implements Stringable
{
private const WHATWG_SPECIAL_SCHEMES = ['ftp', 'http', 'https', 'ws', 'wss'];
private const REGEXP_ENCODED_CHARS = ',%(2[D|E]|3\d|4[1-9|A-F]|5[\d|AF]|6[1-9|A-F]|7[\d|E]),i';

/**
* @var array<string,int>
*/
private const DOT_SEGMENTS = ['.' => 1, '..' => 1];

public readonly ?UriInterface $origin;
public readonly Psr7UriInterface|UriInterface|null $origin;
private readonly ?string $nullValue;

private function __construct(
public readonly Psr7UriInterface|UriInterface $value
) {
$origin = UriInfo::getOrigin($this->value);
if (null !== $origin) {
$this->origin = Uri::new($origin);
$this->nullValue = $this->value instanceof Psr7UriInterface ? '' : null;
$this->origin = $this->computeOrigin($this->value, $this->nullValue);
}

private function computeOrigin(Psr7UriInterface|UriInterface $uri, ?string $nullValue): Psr7UriInterface|UriInterface|null
{
$scheme = $uri->getScheme();
if ('blob' === $scheme) {
$uri = Uri::new($uri->getPath());
$scheme = $uri->getScheme();
}

if (!in_array($scheme, self::WHATWG_SPECIAL_SCHEMES, true)) {
return null;
}

return $uri
->withFragment($nullValue)
->withQuery($nullValue)
->withPath('')
->withUserInfo($nullValue);
}

public static function new(Stringable|string $baseUri): self
Expand All @@ -55,12 +76,96 @@ public function __toString(): string
return $this->value->__toString();
}

public function isAbsolute(): bool
{
return $this->nullValue !== $this->value->getScheme();
}

public function isNetworkPath(): bool
{
return $this->nullValue === $this->value->getScheme()
&& $this->nullValue !== $this->value->getAuthority();
}

public function isAbsolutePath(): bool
{
return $this->nullValue === $this->value->getScheme()
&& $this->nullValue === $this->value->getAuthority()
&& '/' === ($this->value->getPath()[0] ?? '');
}

public function isRelativePath(): bool
{
return $this->nullValue === $this->value->getScheme()
&& $this->nullValue === $this->value->getAuthority()
&& '/' !== ($this->value->getPath()[0] ?? '');
}

/**
* Tells whether both URI refers to the same document.
*/
public function isSameDocument(Stringable|string $uri): bool
{
return self::normalize(self::filterUri($uri)) === self::normalize($this->value);
}

/**
* Normalizes a URI for comparison.
*/
private static function normalize(Psr7UriInterface|UriInterface $uri): string
{
$null = $uri instanceof Psr7UriInterface ? '' : null;

$path = $uri->getPath();
if ('/' === ($path[0] ?? '') || '' !== $uri->getScheme().$uri->getAuthority()) {
$path = BaseUri::new($uri->withPath('')->withQuery($null))->resolve($uri)->value->getPath();
}

$query = $uri->getQuery();
$pairs = null === $query ? [] : explode('&', $query);
sort($pairs);

$value = preg_replace_callback(
self::REGEXP_ENCODED_CHARS,
static fn (array $matches): string => rawurldecode($matches[0]),
[$path, implode('&', $pairs)]
);

if (null !== $value) {
[$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();
}

/**
* Tells whether two URI do not share the same origin.
*
* @see UriInfo::getOrigin()
*/
public function isCrossOrigin(Stringable|string $uri): bool
{
return null === $this->origin
|| null === ($uriOrigin = $this->computeOrigin(Uri::new($uri), null))
|| $uriOrigin->__toString() !== $this->origin->__toString();
}

/**
* Input URI normalization to allow Stringable and string URI.
*/
private static function filterUri(Stringable|string $uri): Psr7UriInterface|UriInterface
{
return match (true) {
$uri instanceof self => $uri->value,
$uri instanceof Psr7UriInterface, $uri instanceof UriInterface => $uri,
default => Uri::new($uri),
};
Expand Down Expand Up @@ -284,7 +389,7 @@ private static function formatHost(Psr7UriInterface|UriInterface $uri): Psr7UriI
*/
private function isRelativizable(Psr7UriInterface|UriInterface $uri): bool
{
return !UriInfo::isRelativePath($uri)
return !self::new($uri)->isRelativePath()
&& self::componentEquals('scheme', $uri)
&& self::componentEquals('authority', $uri);
}
Expand Down Expand Up @@ -358,16 +463,4 @@ private static function formatPathWithEmptyBaseQuery(string $path): string

return '' === $basename ? './' : $basename;
}

/**
* Tells whether two URI do not share the same origin.
*
* @see UriInfo::getOrigin()
*/
public function isCrossOrigin(Stringable|string $uri): bool
{
return null === $this->origin
|| null === ($uriString = UriInfo::getOrigin($uri))
|| $uriString !== $this->origin->toString();
}
}
210 changes: 210 additions & 0 deletions BaseUriTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace League\Uri;

use PHPUnit\Framework\TestCase;
use Psr\Http\Message\UriInterface as Psr7UriInterface;

/**
* @group modifier
Expand Down Expand Up @@ -166,4 +167,213 @@ public static function relativizeAndResolveProvider(): array
'path with colon' => ['http://a/', 'http://a/d:p', './d:p', 'http://a/d:p'],
];
}

/**
* @dataProvider uriProvider
*
* @param array<bool> $infos
*/
public function testInfo(
Psr7UriInterface|Uri $uri,
Psr7UriInterface|Uri|null $base_uri,
array $infos
): void {
if (null !== $base_uri) {
self::assertSame($infos['same_document'], BaseUri::new($base_uri)->isSameDocument($uri));
}
self::assertSame($infos['relative_path'], BaseUri::new($uri)->isRelativePath());
self::assertSame($infos['absolute_path'], BaseUri::new($uri)->isAbsolutePath());
self::assertSame($infos['absolute_uri'], BaseUri::new($uri)->isAbsolute());
self::assertSame($infos['network_path'], BaseUri::new($uri)->isNetworkPath());
}

public static function uriProvider(): array
{
return [
'absolute uri' => [
'uri' => Http::new('http://a/p?q#f'),
'base_uri' => null,
'infos' => [
'absolute_uri' => true,
'network_path' => false,
'absolute_path' => false,
'relative_path' => false,
'same_document' => false,
],
],
'network relative uri' => [
'uri' => Http::new('//스타벅스코리아.com/p?q#f'),
'base_uri' => Http::new('//xn--oy2b35ckwhba574atvuzkc.com/p?q#z'),
'infos' => [
'absolute_uri' => false,
'network_path' => true,
'absolute_path' => false,
'relative_path' => false,
'same_document' => true,
],
],
'path relative uri with non empty path' => [
'uri' => Http::new('p?q#f'),
'base_uri' => null,
'infos' => [
'absolute_uri' => false,
'network_path' => false,
'absolute_path' => false,
'relative_path' => true,
'same_document' => false,
],
],
'path relative uri with empty' => [
'uri' => Http::new('?q#f'),
'base_uri' => null,
'infos' => [
'absolute_uri' => false,
'network_path' => false,
'absolute_path' => false,
'relative_path' => true,
'same_document' => false,
],
],
];
}

public function testIsFunctionsThrowsTypeError(): void
{
self::assertTrue(BaseUri::new('http://example.com')->isAbsolute());
self::assertFalse(BaseUri::new('http://example.com')->isNetworkPath());
self::assertTrue(BaseUri::new('/example.com')->isAbsolutePath());
self::assertTrue(BaseUri::new('example.com#foobar')->isRelativePath());
}

/**
* @dataProvider sameValueAsProvider
*/
public function testSameValueAs(Psr7UriInterface|Uri $uri1, Psr7UriInterface|Uri $uri2, bool $expected): void
{
self::assertSame($expected, BaseUri::new($uri2)->isSameDocument($uri1));
}

public static function sameValueAsProvider(): array
{
return [
'2 disctincts URIs' => [
Http::new('http://example.com'),
Uri::new('ftp://example.com'),
false,
],
'2 identical URIs' => [
Http::new('http://example.com'),
Http::new('http://example.com'),
true,
],
'2 identical URIs after removing dot segment' => [
Http::new('http://example.org/~foo/'),
Http::new('http://example.ORG/bar/./../~foo/'),
true,
],
'2 distincts relative URIs' => [
Http::new('~foo/'),
Http::new('../~foo/'),
false,
],
'2 identical relative URIs' => [
Http::new('../%7efoo/'),
Http::new('../~foo/'),
true,
],
'2 identical URIs after normalization (1)' => [
Http::new('HtTp://مثال.إختبار:80/%7efoo/%7efoo/'),
Http::new('http://xn--mgbh0fb.xn--kgbechtv/%7Efoo/~foo/'),
true,
],
'2 identical URIs after normalization (2)' => [
Http::new('http://www.example.com'),
Http::new('http://www.example.com/'),
true,
],
'2 identical URIs after normalization (3)' => [
Http::new('http://www.example.com'),
Http::new('http://www.example.com:/'),
true,
],
'2 identical URIs after normalization (4)' => [
Http::new('http://www.example.com'),
Http::new('http://www.example.com:80/'),
true,
],
];
}

/**
* @dataProvider getOriginProvider
*/
public function testGetOrigin(Psr7UriInterface|Uri $uri, ?string $expectedOrigin): void
{
self::assertSame($expectedOrigin, BaseUri::new($uri)->origin?->__toString());
}

public static function getOriginProvider(): array
{
return [
'http uri' => [
'uri' => Uri::new('https://example.com/path?query#fragment'),
'expectedOrigin' => 'https://example.com',
],
'http uri with non standard port' => [
'uri' => Uri::new('https://example.com:81/path?query#fragment'),
'expectedOrigin' => 'https://example.com:81',
],
'relative uri' => [
'uri' => Uri::new('//example.com:81/path?query#fragment'),
'expectedOrigin' => null,
],
'absolute uri with user info' => [
'uri' => Uri::new('https://user:[email protected]:81/path?query#fragment'),
'expectedOrigin' => 'https://example.com:81',
],
'opaque URI' => [
'uri' => Uri::new('mailto:[email protected]'),
'expectedOrigin' => null,
],
'file URI' => [
'uri' => Uri::new('file:///usr/bin/test'),
'expectedOrigin' => null,
],
'blob' => [
'uri' => Uri::new('blob:https://mozilla.org:443/'),
'expectedOrigin' => 'https://mozilla.org',
],
];
}

/**
* @dataProvider getCrossOriginExamples
*/
public function testIsCrossOrigin(string $original, string $modified, bool $expected): void
{
self::assertSame($expected, BaseUri::new($original)->isCrossOrigin($modified));
}

/**
* @return array<string, array{0:string, 1:string, 2:bool}>
*/
public static function getCrossOriginExamples(): array
{
return [
'different path' => ['http://example.com/123', 'http://example.com/', false],
'same port with default value (1)' => ['https://example.com/123', 'https://example.com:443/', false],
'same port with default value (2)' => ['ws://example.com:80/123', 'ws://example.com/', false],
'same explicit port' => ['wss://example.com:443/123', 'wss://example.com:443/', false],
'same origin with i18n host' => ['https://xn--bb-bjab.be./path', 'https://Bébé.BE./path', false],
'same origin using a blob' => ['blob:https://mozilla.org:443/', 'https://mozilla.org/123', false],
'different scheme' => ['https://example.com/123', 'ftp://example.com/', true],
'different host' => ['ftp://example.com/123', 'ftp://www.example.com/123', true],
'different port implicit' => ['https://example.com/123', 'https://example.com:81/', true],
'different port explicit' => ['https://example.com:80/123', 'https://example.com:81/', true],
'same scheme different port' => ['https://example.com:443/123', 'https://example.com:444/', true],
'comparing two opaque URI' => ['ldap://ldap.example.net', 'ldap://ldap.example.net', true],
'comparing a URI with an origin and one with an opaque origin' => ['https://example.com:443/123', 'ldap://ldap.example.net', true],
'cross origin using a blob' => ['blob:http://mozilla.org:443/', 'https://mozilla.org/123', true],
];
}
}
Loading

0 comments on commit 4059b3b

Please sign in to comment.