* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Uri; use League\Uri\Contracts\UriException; use League\Uri\Exceptions\SyntaxError; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Stringable; use function rawurlencode; #[CoversClass(UriString::class)] final class UriStringTest extends TestCase { #[DataProvider('validUriProvider')] public function testParseSucced(Stringable|string|int $uri, array $expected): void { self::assertSame($expected, UriString::parse($uri)); } public static function validUriProvider(): array { return [ 'scheme with non-leading digit' => [ 's3://somebucket/somefile.txt', [ 'scheme' => 's3', 'user' => null, 'pass' => null, 'host' => 'somebucket', 'port' => null, 'path' => '/somefile.txt', 'query' => null, 'fragment' => null, ], ], 'uri with host ascii version' => [ 'scheme://user:pass@xn--mgbh0fb.xn--kgbechtv', [ 'scheme' => 'scheme', 'user' => 'user', 'pass' => 'pass', 'host' => 'xn--mgbh0fb.xn--kgbechtv', 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ], ], 'complete URI' => [ 'scheme://user:pass@host:81/path?query#fragment', [ 'scheme' => 'scheme', 'user' => 'user', 'pass' => 'pass', 'host' => 'host', 'port' => 81, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI is not normalized' => [ 'ScheMe://user:pass@HoSt:81/path?query#fragment', [ 'scheme' => 'ScheMe', 'user' => 'user', 'pass' => 'pass', 'host' => 'HoSt', 'port' => 81, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without scheme' => [ '//user:pass@HoSt:81/path?query#fragment', [ 'scheme' => null, 'user' => 'user', 'pass' => 'pass', 'host' => 'HoSt', 'port' => 81, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without empty authority only' => [ '//', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => '', 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ], ], 'URI without userinfo' => [ 'scheme://HoSt:81/path?query#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => 'HoSt', 'port' => 81, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI with empty userinfo' => [ 'scheme://@HoSt:81/path?query#fragment', [ 'scheme' => 'scheme', 'user' => '', 'pass' => null, 'host' => 'HoSt', 'port' => 81, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without port' => [ 'scheme://user:pass@host/path?query#fragment', [ 'scheme' => 'scheme', 'user' => 'user', 'pass' => 'pass', 'host' => 'host', 'port' => null, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI with an empty port' => [ 'scheme://user:pass@host:/path?query#fragment', [ 'scheme' => 'scheme', 'user' => 'user', 'pass' => 'pass', 'host' => 'host', 'port' => null, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without user info and port' => [ 'scheme://host/path?query#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => 'host', 'port' => null, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI with host IP' => [ 'scheme://10.0.0.2/p?q#f', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => '10.0.0.2', 'port' => null, 'path' => '/p', 'query' => 'q', 'fragment' => 'f', ], ], 'URI with scoped IP' => [ 'scheme://[fe80:1234::%251]/p?q#f', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => '[fe80:1234::%251]', 'port' => null, 'path' => '/p', 'query' => 'q', 'fragment' => 'f', ], ], 'URI with IP future' => [ 'scheme://[vAF.1::2::3]/p?q#f', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => '[vAF.1::2::3]', 'port' => null, 'path' => '/p', 'query' => 'q', 'fragment' => 'f', ], ], 'URI without authority' => [ 'scheme:path?query#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without authority and scheme' => [ '/path', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '/path', 'query' => null, 'fragment' => null, ], ], 'URI with empty host' => [ 'scheme:///path?query#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => '', 'port' => null, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI with empty host and without scheme' => [ '///path?query#fragment', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => '', 'port' => null, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without path' => [ 'scheme://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]?query#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => '[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]', 'port' => null, 'path' => '', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without path and scheme' => [ '//[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]?query#fragment', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => '[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]', 'port' => null, 'path' => '', 'query' => 'query', 'fragment' => 'fragment', ], ], 'URI without scheme with IPv6 host and port' => [ '//[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:42?query#fragment', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => '[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]', 'port' => 42, 'path' => '', 'query' => 'query', 'fragment' => 'fragment', ], ], 'complete URI without scheme' => [ '//user@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:42?q#f', [ 'scheme' => null, 'user' => 'user', 'pass' => null, 'host' => '[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]', 'port' => 42, 'path' => '', 'query' => 'q', 'fragment' => 'f', ], ], 'URI without authority and query' => [ 'scheme:path#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'path', 'query' => null, 'fragment' => 'fragment', ], ], 'URI with empty query' => [ 'scheme:path?#fragment', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'path', 'query' => '', 'fragment' => 'fragment', ], ], 'URI with query only' => [ '?query', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => 'query', 'fragment' => null, ], ], 'URI without fragment' => [ 'tel:05000', [ 'scheme' => 'tel', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '05000', 'query' => null, 'fragment' => null, ], ], 'URI with empty fragment' => [ 'scheme:path#', [ 'scheme' => 'scheme', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'path', 'query' => null, 'fragment' => '', ], ], 'URI with fragment only' => [ '#fragment', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => null, 'fragment' => 'fragment', ], ], 'URI with empty fragment only' => [ '#', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => null, 'fragment' => '', ], ], 'URI without authority 2' => [ 'path#fragment', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'path', 'query' => null, 'fragment' => 'fragment', ], ], 'URI with empty query and fragment' => [ '?#', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => '', 'fragment' => '', ], ], 'URI with absolute path' => [ '/?#', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '/', 'query' => '', 'fragment' => '', ], ], 'URI with absolute authority' => [ 'https://thephpleague.com./p?#f', [ 'scheme' => 'https', 'user' => null, 'pass' => null, 'host' => 'thephpleague.com.', 'port' => null, 'path' => '/p', 'query' => '', 'fragment' => 'f', ], ], 'URI with absolute path only' => [ '/', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '/', 'query' => null, 'fragment' => null, ], ], 'URI with empty query only' => [ '?', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => '', 'fragment' => null, ], ], 'relative path' => [ '../relative/path', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '../relative/path', 'query' => null, 'fragment' => null, ], ], 'complex authority' => [ 'http://a_.!~*\'(-)n0123Di%25%26:pass;:&=+$,word@www.zend.com', [ 'scheme' => 'http', 'user' => 'a_.!~*\'(-)n0123Di%25%26', 'pass' => 'pass;:&=+$,word', 'host' => 'www.zend.com', 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ], ], 'complex authority without scheme' => [ '//a_.!~*\'(-)n0123Di%25%26:pass;:&=+$,word@www.zend.com', [ 'scheme' => null, 'user' => 'a_.!~*\'(-)n0123Di%25%26', 'pass' => 'pass;:&=+$,word', 'host' => 'www.zend.com', 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ], ], 'single word is a path' => [ 'http', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'http', 'query' => null, 'fragment' => null, ], ], 'URI scheme with an empty authority' => [ 'http://', [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => '', 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ], ], 'single word is a path, no' => [ 'http:::/path', [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '::/path', 'query' => null, 'fragment' => null, ], ], 'fragment with pseudo segment' => [ 'http://example.com#foo=1/bar=2', [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => 'example.com', 'port' => null, 'path' => '', 'query' => null, 'fragment' => 'foo=1/bar=2', ], ], 'complex URI' => [ 'htà+d/s:totot', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => 'htà+d/s:totot', 'query' => null, 'fragment' => null, ], ], 'scheme only URI' => [ 'http:', [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ], ], 'RFC3986 LDAP example' => [ 'ldap://[2001:db8::7]/c=GB?objectClass?one', [ 'scheme' => 'ldap', 'user' => null, 'pass' => null, 'host' => '[2001:db8::7]', 'port' => null, 'path' => '/c=GB', 'query' => 'objectClass?one', 'fragment' => null, ], ], 'RFC3987 example' => [ 'http://bébé.bé./有词法别名.zh', [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => 'bébé.bé.', 'port' => null, 'path' => '/有词法别名.zh', 'query' => null, 'fragment' => null, ], ], 'colon detection respect RFC3986 (1)' => [ 'http://example.org/hello:12?foo=bar#test', [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => 'example.org', 'port' => null, 'path' => '/hello:12', 'query' => 'foo=bar', 'fragment' => 'test', ], ], 'colon detection respect RFC3986 (2)' => [ '/path/to/colon:34', [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '/path/to/colon:34', 'query' => null, 'fragment' => null, ], ], 'scheme with hyphen' => [ 'android-app://org.wikipedia/http/en.m.wikipedia.org/wiki/The_Hitchhiker%27s_Guide_to_the_Galaxy', [ 'scheme' => 'android-app', 'user' => null, 'pass' => null, 'host' => 'org.wikipedia', 'port' => null, 'path' => '/http/en.m.wikipedia.org/wiki/The_Hitchhiker%27s_Guide_to_the_Galaxy', 'query' => null, 'fragment' => null, ], ], 'URI is a scalar value' => [ 1234, [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '1234', 'query' => null, 'fragment' => null, ], ], 'URI is a object with __toString' => [ new class () { public function __toString(): string { return 'http://example.org/hello:12?foo=bar#test'; } }, [ 'scheme' => 'http', 'user' => null, 'pass' => null, 'host' => 'example.org', 'port' => null, 'path' => '/hello:12', 'query' => 'foo=bar', 'fragment' => 'test', ], ], 'Authority is the colon' => [ 'ftp://:/p?q#f', [ 'scheme' => 'ftp', 'user' => null, 'pass' => null, 'host' => '', 'port' => null, 'path' => '/p', 'query' => 'q', 'fragment' => 'f', ], ], 'URI with 0 leading port' => [ 'scheme://user:pass@host:000000000081/path?query#fragment', [ 'scheme' => 'scheme', 'user' => 'user', 'pass' => 'pass', 'host' => 'host', 'port' => 81, 'path' => '/path', 'query' => 'query', 'fragment' => 'fragment', ], ], ]; } #[DataProvider('invalidUriProvider')] public function testParseFailed(string $uri): void { self::expectException(SyntaxError::class); UriString::parse($uri); } public static function invalidUriProvider(): array { return [ 'invalid scheme' => ['0scheme://host/path?query#fragment'], 'invalid path' => ['://host:80/p?q#f'], 'invalid port (1)' => ['//host:port/path?query#fragment'], 'invalid port (2)' => ['//host:-892358/path?query#fragment'], 'invalid host' => ['http://exam ple.com'], 'invalid ipv6 host (1)' => ['scheme://[127.0.0.1]/path?query#fragment'], 'invalid ipv6 host (2)' => ['scheme://]::1[/path?query#fragment'], 'invalid ipv6 host (3)' => ['scheme://[::1|/path?query#fragment'], 'invalid ipv6 host (4)' => ['scheme://|::1]/path?query#fragment'], 'invalid ipv6 host (5)' => ['scheme://[::1]./path?query#fragment'], 'invalid ipv6 host (6)' => ['scheme://[[::1]]:80/path?query#fragment'], 'invalid ipv6 scoped (1)' => ['scheme://[::1%25%23]/path?query#fragment'], 'invalid ipv6 scoped (2)' => ['scheme://[fe80::1234::%251]/path?query#fragment'], 'invalid char on URI' => ["scheme://host/path/\r\n/toto"], 'invalid path only URI' => ['2620:0:1cfe:face:b00c::3'], 'invalid path PHP bug #72811' => ['[::1]:80'], 'invalid ipvfuture' => ['//[v6.::1]/p?q#f'], 'invalid RFC3987 host' => ['//a⒈com/p?q#f'], 'invalid RFC3987 host URL encoded' => ['//'.rawurlencode('a⒈com').'/p?q#f'], 'invalid Host with fullwith (1)' => ['http://%00.com'], 'invalid host with fullwidth escaped' => ['http://%ef%bc%85%ef%bc%94%ef%bc%91.com],'], //'invalid pseudo IDN to ASCII string' => ['http://xn--3/foo.'], 'invalid IDN' => ['//:�@�����������������������������������������������������������������������������������������/'], ]; } #[DataProvider('buildUriProvider')] public function testBuild(string $uri, string $expected): void { self::assertSame($expected, UriString::build(UriString::parse($uri))); } public static function buildUriProvider(): array { return [ 'complete URI' => [ 'scheme://user:pass@host:81/path?query#fragment', 'scheme://user:pass@host:81/path?query#fragment', ], 'URI is not normalized' => [ 'ScheMe://user:pass@HoSt:81/path?query#fragment', 'ScheMe://user:pass@HoSt:81/path?query#fragment', ], 'URI without scheme' => [ '//user:pass@HoSt:81/path?query#fragment', '//user:pass@HoSt:81/path?query#fragment', ], 'URI without empty authority only' => [ '//', '//', ], 'URI without userinfo' => [ 'scheme://HoSt:81/path?query#fragment', 'scheme://HoSt:81/path?query#fragment', ], 'URI with empty userinfo' => [ 'scheme://@HoSt:81/path?query#fragment', 'scheme://@HoSt:81/path?query#fragment', ], 'URI without port' => [ 'scheme://user:pass@host/path?query#fragment', 'scheme://user:pass@host/path?query#fragment', ], 'URI with an empty port' => [ 'scheme://user:pass@host:/path?query#fragment', 'scheme://user:pass@host/path?query#fragment', ], 'URI without user info and port' => [ 'scheme://host/path?query#fragment', 'scheme://host/path?query#fragment', ], 'URI with host IP' => [ 'scheme://10.0.0.2/p?q#f', 'scheme://10.0.0.2/p?q#f', ], 'URI with scoped IP' => [ 'scheme://[fe80:1234::%251]/p?q#f', 'scheme://[fe80:1234::%251]/p?q#f', ], 'URI without authority' => [ 'scheme:path?query#fragment', 'scheme:path?query#fragment', ], 'URI without authority and scheme' => [ '/path', '/path', ], 'URI with empty host' => [ 'scheme:///path?query#fragment', 'scheme:///path?query#fragment', ], 'URI with empty host and without scheme' => [ '///path?query#fragment', '///path?query#fragment', ], 'URI without path' => [ 'scheme://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]?query#fragment', 'scheme://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]?query#fragment', ], 'URI without path and scheme' => [ '//[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]?query#fragment', '//[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]?query#fragment', ], 'URI without scheme with IPv6 host and port' => [ '//[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:42?query#fragment', '//[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:42?query#fragment', ], 'complete URI without scheme' => [ '//user@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:42?q#f', '//user@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:42?q#f', ], 'URI without authority and query' => [ 'scheme:path#fragment', 'scheme:path#fragment', ], 'URI with empty query' => [ 'scheme:path?#fragment', 'scheme:path?#fragment', ], 'URI with query only' => [ '?query', '?query', ], 'URI without fragment' => [ 'tel:05000', 'tel:05000', ], 'URI with empty fragment' => [ 'scheme:path#', 'scheme:path#', ], 'URI with fragment only' => [ '#fragment', '#fragment', ], 'URI with empty fragment only' => [ '#', '#', ], 'URI without authority 2' => [ 'path#fragment', 'path#fragment', ], 'URI with empty query and fragment' => [ '?#', '?#', ], 'URI with absolute path' => [ '/?#', '/?#', ], 'URI with absolute authority' => [ 'https://thephpleague.com./p?#f', 'https://thephpleague.com./p?#f', ], 'URI with absolute path only' => [ '/', '/', ], 'URI with empty query only' => [ '?', '?', ], 'relative path' => [ '../relative/path', '../relative/path', ], 'complex authority' => [ 'http://a_.!~*\'(-)n0123Di%25%26:pass;:&=+$,word@www.zend.com', 'http://a_.!~*\'(-)n0123Di%25%26:pass;:&=+$,word@www.zend.com', ], 'complex authority without scheme' => [ '//a_.!~*\'(-)n0123Di%25%26:pass;:&=+$,word@www.zend.com', '//a_.!~*\'(-)n0123Di%25%26:pass;:&=+$,word@www.zend.com', ], 'single word is a path' => [ 'http', 'http', ], 'URI scheme with an empty authority' => [ 'http://', 'http://', ], 'single word is a path, no' => [ 'http:::/path', 'http:::/path', ], 'fragment with pseudo segment' => [ 'http://example.com#foo=1/bar=2', 'http://example.com#foo=1/bar=2', ], 'complex URI' => [ 'htà+d/s:totot', 'htà+d/s:totot', ], 'scheme only URI' => [ 'http:', 'http:', ], 'RFC3986 LDAP example' => [ 'ldap://[2001:db8::7]/c=GB?objectClass?one', 'ldap://[2001:db8::7]/c=GB?objectClass?one', ], 'RFC3987 example' => [ 'http://bébé.bé./有词法别名.zh', 'http://bébé.bé./有词法别名.zh', ], 'colon detection respect RFC3986 (1)' => [ 'http://example.org/hello:12?foo=bar#test', 'http://example.org/hello:12?foo=bar#test', ], 'colon detection respect RFC3986 (2)' => [ '/path/to/colon:34', '/path/to/colon:34', ], 'scheme with hyphen' => [ 'android-app://org.wikipedia/http/en.m.wikipedia.org/wiki/The_Hitchhiker%27s_Guide_to_the_Galaxy', 'android-app://org.wikipedia/http/en.m.wikipedia.org/wiki/The_Hitchhiker%27s_Guide_to_the_Galaxy', ], ]; } #[DataProvider('invalidAuthorityComponents')] public function test_it_will_fails_reconstructing_the_uri_with_invalid_authority(array $components): void { $this->expectException(SyntaxError::class); UriString::buildAuthority($components); } public static function invalidAuthorityComponents(): iterable { yield 'missing host but has port' => [ 'components' => ['port' => 80], ]; yield 'missing host but has user component' => [ 'components' => ['user' => 'foo'], ]; yield 'missing host but has pass component' => [ 'components' => ['pass' => 'bar'], ]; } #[DataProvider('resolveProvider')] public function testCreateResolve(string $baseUri, string $uri, string $expected): void { self::assertSame($expected, UriString::resolve($uri, $baseUri)); } public static function resolveProvider(): array { $baseUri = 'http://a/b/c/d;p?q'; return [ 'base uri' => [$baseUri, '', $baseUri], 'scheme' => [$baseUri, 'http://d/e/f', 'http://d/e/f'], 'path 1' => [$baseUri, 'g', 'http://a/b/c/g'], 'path 2' => [$baseUri, './g', 'http://a/b/c/g'], 'path 3' => [$baseUri, 'g/', 'http://a/b/c/g/'], 'path 4' => [$baseUri, '/g', 'http://a/g'], 'authority' => [$baseUri, '//g', 'http://g'], 'query' => [$baseUri, '?y', 'http://a/b/c/d;p?y'], 'path + query' => [$baseUri, 'g?y', 'http://a/b/c/g?y'], 'fragment' => [$baseUri, '#s', 'http://a/b/c/d;p?q#s'], 'path + fragment' => [$baseUri, 'g#s', 'http://a/b/c/g#s'], 'path + query + fragment' => [$baseUri, 'g?y#s', 'http://a/b/c/g?y#s'], 'single dot 1' => [$baseUri, '.', 'http://a/b/c/'], 'single dot 2' => [$baseUri, './', 'http://a/b/c/'], 'single dot 3' => [$baseUri, './g/.', 'http://a/b/c/g/'], 'single dot 4' => [$baseUri, 'g/./h', 'http://a/b/c/g/h'], 'double dot 1' => [$baseUri, '..', 'http://a/b/'], 'double dot 2' => [$baseUri, '../', 'http://a/b/'], 'double dot 3' => [$baseUri, '../g', 'http://a/b/g'], 'double dot 4' => [$baseUri, '../..', 'http://a/'], 'double dot 5' => [$baseUri, '../../', 'http://a/'], 'double dot 6' => [$baseUri, '../../g', 'http://a/g'], 'double dot 7' => [$baseUri, '../../../g', 'http://a/g'], 'double dot 8' => [$baseUri, '../../../../g', 'http://a/g'], 'double dot 9' => [$baseUri, 'g/../h' , 'http://a/b/c/h'], 'mulitple slashes' => [$baseUri, 'foo////g', 'http://a/b/c/foo////g'], 'complex path 1' => [$baseUri, ';x', 'http://a/b/c/;x'], 'complex path 2' => [$baseUri, 'g;x', 'http://a/b/c/g;x'], 'complex path 3' => [$baseUri, 'g;x?y#s', 'http://a/b/c/g;x?y#s'], 'complex path 4' => [$baseUri, 'g;x=1/./y', 'http://a/b/c/g;x=1/y'], 'complex path 5' => [$baseUri, 'g;x=1/../y', 'http://a/b/c/y'], 'dot segments presence 1' => [$baseUri, '/./g', 'http://a/g'], 'dot segments presence 2' => [$baseUri, '/../g', 'http://a/g'], 'dot segments presence 3' => [$baseUri, 'g.', 'http://a/b/c/g.'], 'dot segments presence 4' => [$baseUri, '.g', 'http://a/b/c/.g'], 'dot segments presence 5' => [$baseUri, 'g..', 'http://a/b/c/g..'], 'dot segments presence 6' => [$baseUri, '..g', 'http://a/b/c/..g'], 'origin uri without path' => ['http://h:b@a', 'b/../y', 'http://h:b@a/y'], 'not same origin' => [$baseUri, 'ftp://a/b/c/d', 'ftp://a/b/c/d'], ]; } #[DataProvider('extraResolutionProvider')] public function test_rfc3986_resolution(string $baseUri, string $expected, string $uri): void { self::assertSame($expected, UriString::resolve($uri, $baseUri)); } /** * @return iterable> */ public static function extraResolutionProvider(): iterable { $baseUri = 'http://a/b/c/d;p?q'; // From RFC 3986. yield [$baseUri, 'g:h', 'g:h']; yield [$baseUri, 'http://a/b/c/g', 'g']; yield [$baseUri, 'http://a/b/c/g', './g']; yield [$baseUri, 'http://a/b/c/g/', 'g/']; yield [$baseUri, 'http://a/g', '/g']; yield [$baseUri, 'http://g', '//g']; yield [$baseUri, 'http://a/b/c/d;p?y', '?y']; yield [$baseUri, 'http://a/b/c/g?y', 'g?y']; yield [$baseUri, 'http://a/b/c/d;p?q#s', '#s']; yield [$baseUri, 'http://a/b/c/g#s', 'g#s']; yield [$baseUri, 'http://a/b/c/g?y#s', 'g?y#s']; yield [$baseUri, 'http://a/b/c/;x', ';x']; yield [$baseUri, 'http://a/b/c/g;x', 'g;x']; yield [$baseUri, 'http://a/b/c/g;x?y#s', 'g;x?y#s']; yield [$baseUri, 'http://a/b/c/d;p?q', '']; yield [$baseUri, 'http://a/b/c/', '.']; yield [$baseUri, 'http://a/b/c/', './']; yield [$baseUri, 'http://a/b/', '..']; yield [$baseUri, 'http://a/b/', '../']; yield [$baseUri, 'http://a/b/g', '../g']; yield [$baseUri, 'http://a/', '../..']; yield [$baseUri, 'http://a/', '../../']; yield [$baseUri, 'http://a/g', '../../g']; yield [$baseUri, 'http://a/g', '../../../g']; yield [$baseUri, 'http://a/g', '../../../../g']; yield [$baseUri, 'http://a/g', '/./g']; yield [$baseUri, 'http://a/g', '/../g']; yield [$baseUri, 'http://a/b/c/g.', 'g.']; yield [$baseUri, 'http://a/b/c/.g', '.g']; yield [$baseUri, 'http://a/b/c/g..', 'g..']; yield [$baseUri, 'http://a/b/c/..g', '..g']; yield [$baseUri, 'http://a/b/g', './../g']; yield [$baseUri, 'http://a/b/c/g/', './g/.']; yield [$baseUri, 'http://a/b/c/g/h', 'g/./h']; yield [$baseUri, 'http://a/b/c/h', 'g/../h']; yield [$baseUri, 'http://a/b/c/g;x=1/y', 'g;x=1/./y']; yield [$baseUri, 'http://a/b/c/y', 'g;x=1/../y']; yield [$baseUri, 'http://a/b/c/g?y/./x', 'g?y/./x']; yield [$baseUri, 'http://a/b/c/g?y/../x', 'g?y/../x']; yield [$baseUri, 'http://a/b/c/g#s/./x', 'g#s/./x']; yield [$baseUri, 'http://a/b/c/g#s/../x', 'g#s/../x']; // Additional tests (not from RFC 3986). yield [$baseUri, 'foo:g', 'foo:g']; yield [$baseUri, 'http://a/b/g;p/h;s', '../g;p/h;s']; $baseUri = 'http://a?q'; yield [$baseUri, 'http://a?p', '?p']; yield [$baseUri, 'http://a/b/c', 'b/c']; $baseUri = 'foo:/a/b?q'; yield [$baseUri, 'foo:/a/b?p', '?p']; yield [$baseUri, 'foo:/c', '../c']; yield [$baseUri, 'foo:/a/c', 'c']; yield [$baseUri, 'foo:/c', '../../c']; yield [$baseUri, 'foo:/c', '/c']; } #[Test] #[DataProvider('invalidUriWithWhitespaceProvider')] public function it_fails_parsing_uri_string_with_whitespace(string $uri): void { $this->expectException(UriException::class); UriString::parse($uri); } public static function invalidUriWithWhitespaceProvider(): iterable { yield 'uri containing only whitespaces' => ['uri' => ' ']; yield 'uri starting with whitespaces' => ['uri' => ' https://a/b?c']; yield 'uri ending with whitespaces' => ['uri' => 'https://a/b?c ']; yield 'uri surrounded by whitespaces' => ['uri' => ' https://a/b?c ']; yield 'uri containing with whitespaces' => ['uri' => 'https://a/b ?c']; } #[Test] #[DataProvider('normalizedUriProvider')] public function it_can_normalize_uri_string(string $uri, string $expected): void { self::assertSame($expected, UriString::normalize($uri)); } public static function normalizedUriProvider(): iterable { yield 'URI is unchanged' => [ 'uri' => 'http://a/b/c', 'expected' => 'http://a/b/c', ]; yield 'URI scheme is normalized to lowercase' => [ 'uri' => 'HtTp://a/b/c', 'expected' => 'http://a/b/c', ]; yield 'URI host is normalized to lowercase' => [ 'uri' => 'HtTp://AaAa/b/c', 'expected' => 'http://aaaa/b/c', ]; yield 'URI path is partially decoded without affecting delimiter characters' => [ 'uri' => 'https://example.com/foo/bar%2Fbaz', 'expected' => 'https://example.com/foo/bar%2Fbaz', ]; yield 'URI query is partially decoded without affecting delimiter characters' => [ 'uri' => 'https://example.com?foo=bar%26baz%3Dqux', 'expected' => 'https://example.com?foo=bar%26baz%3Dqux', ]; yield 'URI IPv6 host is not compressed' => [ 'uri' => 'https://[fe80:0000:0000:0000:0000:0000:0000:000a%25en1]/foo/bar', 'expected' => 'https://[fe80:0000:0000:0000:0000:0000:0000:000a%25en1]/foo/bar', ]; yield 'URI let port unchanged' => [ 'uri' => 'https://foobar:443/foo/bar', 'expected' => 'https://foobar:443/foo/bar', ]; } public function test_it_does_resolves_uri_against_authority_less_absolute_path(): void { self::assertSame('foo:/c', UriString::resolve('../../c', 'foo:/a/b')); } public function test_it_can_resolve_uri_when_dot_segment_leave_the_path_relative(): void { self::assertSame('https://user:pass@host/toto', UriString::resolve('https://user:pass@host/./.././toto')); } public function test_it_resolve_correctly_uri_issue_184(): void { $baseUri = 'http://a/b/c/d;p?q'; self::assertSame('g:h', UriString::resolve('g:h', $baseUri)); self::assertSame('foo:g', UriString::resolve('foo:g', $baseUri)); } }