diff --git a/CHANGELOG.md b/CHANGELOG.md index b73dd9e1..d6d17af1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All Notable changes to `League\Uri` will be documented in this file ### Added -- None +- `UriInfo::isCrossOrigin` method ### Fixed diff --git a/composer.json b/composer.json index 2059a5f4..4d361cec 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes --ansi", "phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi", "phpstan": "phpstan analyse -l max -c phpstan.neon src --ansi --memory-limit=256M", - "phpunit": "phpunit --coverage-text", + "phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text", "test": [ "@phpunit", "@phpstan", diff --git a/docs/uri/6.0/info.md b/docs/uri/6.0/info.md index e5c7f5b2..34116ef5 100644 --- a/docs/uri/6.0/info.md +++ b/docs/uri/6.0/info.md @@ -78,7 +78,7 @@ UriInfo::getOrigin(Http::createFromString('file:///usr/bin/php')); //returns nul UriInfo::getOrigin(Uri::createFromString('data:text/plain,Bonjour%20le%20monde%21')); //returns null ~~~ -

For absolute URI with the `file` scheme the method will return null (as this is left to the implementation decision)

+

For absolute URI with the file scheme the method will return null (as this is left to the implementation decision)

Because the origin property does not exists in the RFC3986 specification this additional steps is implemented: @@ -92,3 +92,25 @@ use League\Uri\UriInfo; UriInfo::getOrigin(Http::createFromString('/path/to/endpoint')); //returns null ~~~ + +## UriInfo::isCrossOrigin + +This public static method tells whether the given URI object represents different origins. +According to [RFC9110](https://www.rfc-editor.org/rfc/rfc9110#section-4.3.1) The "origin" for a given URI is the triple of scheme, host, and port +after normalizing the scheme and host to lowercase and normalizing the port to remove any leading +zeros. + +~~~php + 21, 'http' => 80, 'https' => 443, 'ws' => 80, 'wss' => 443]; /** * @codeCoverageIgnore @@ -88,9 +88,7 @@ private static function normalize($uri) $pairs = null === $query ? [] : explode('&', $query); sort($pairs, SORT_REGULAR); - $replace = static function (array $matches): string { - return rawurldecode($matches[0]); - }; + $replace = static fn (array $matches): string => rawurldecode($matches[0]); $retval = preg_replace_callback(self::REGEXP_ENCODED_CHARS, $replace, [$path, implode('&', $pairs), $fragment]); if (null !== $retval) { @@ -195,12 +193,23 @@ public static function getOrigin($uri): ?string $scheme = $uri->getScheme(); } - if (in_array($scheme, self::WHATWG_SPECIAL_SCHEMES, true)) { - $null = self::emptyComponentValue($uri); - - return (string) $uri->withFragment($null)->withQuery($null)->withPath('')->withUserInfo($null, null); + if (null === $scheme || !array_key_exists($scheme, self::WHATWG_SPECIAL_SCHEMES)) { + return null; } - return null; + $null = self::emptyComponentValue($uri); + + return (string) $uri->withFragment($null)->withQuery($null)->withPath('')->withUserInfo($null); + } + + /** + * @param Psr7UriInterface|UriInterface $uri + * @param Psr7UriInterface|UriInterface $base_uri + */ + public static function isCrossOrigin($uri, $base_uri): bool + { + return null === ($uriString = self::getOrigin($uri)) + || null === ($baseUriString = self::getOrigin($base_uri)) + || $uriString !== $baseUriString; } } diff --git a/src/UriInfoTest.php b/src/UriInfoTest.php index 5b990643..68d40e80 100644 --- a/src/UriInfoTest.php +++ b/src/UriInfoTest.php @@ -215,4 +215,35 @@ public function getOriginProvider(): array ], ]; } + + /** + * @dataProvider getCrossOriginExamples + */ + public function testIsCrossOrigin(string $original, string $modified, bool $expected): void + { + self::assertSame($expected, UriInfo::isCrossOrigin(Uri::createFromString($original), Http::createFromString($modified))); + } + + /** + * @return array + */ + public 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], + ]; + } }